1use std::collections::BTreeMap;
28use std::sync::{PoisonError, RwLock};
29
30use base64::engine::general_purpose::URL_SAFE_NO_PAD;
31use base64::Engine;
32use pasetors::token::UntrustedToken;
33use pasetors::version4::V4;
34use sha2::{Digest, Sha256};
35
36use cheers_core::{
37 evaluate_admit, Admit, AdmissionPolicy, AdmissionRefusal, AdmitFacts, CodecError, DeviceSigned, KnockError, LeaseRequest, LeaseState, PeerKey, PrincipalId, PrincipalKind, StoreError, UserId,
38};
39
40use crate::key_set::VerifyError;
41use crate::public_verifier::PasetoV4PublicVerifier;
42use crate::revocation::RevocationReader;
43use crate::snapshot::{SnapshotError, SnapshotVerifier, VerifiedSnapshot};
44use crate::standing::{StandingError, StandingVerifier};
45
46pub fn verify_device_artifact<T: DeviceSigned>(token: &str) -> Result<T, CodecError> {
53 let untrusted = UntrustedToken::<pasetors::token::Public, V4>::try_from(token).map_err(|_| CodecError::Malformed)?;
54 let claimed: T = serde_json::from_slice(untrusted.untrusted_payload())?;
55 let signer = claimed.signer();
56 if signer.kind != PrincipalKind::Key {
57 return Err(CodecError::Malformed);
58 }
59 let key = key_bytes(signer).ok_or(CodecError::Malformed)?;
60 PasetoV4PublicVerifier::from_public_key(&key)?.verify_artifact(token)
61}
62
63fn key_bytes(p: &PrincipalId) -> Option<[u8; 32]> {
65 if p.kind != PrincipalKind::Key {
66 return None;
67 }
68 URL_SAFE_NO_PAD.decode(p.id.as_bytes()).ok()?.try_into().ok()
69}
70
71pub fn artifact_hash(token: &str) -> String {
74 URL_SAFE_NO_PAD.encode(Sha256::digest(token.as_bytes()))
75}
76
77#[derive(Debug, thiserror::Error)]
79pub enum AdmitError {
80 #[error("admit refused: {0}")]
82 Signature(#[from] CodecError),
83 #[error("admit refused: {0}")]
85 Invalid(#[from] KnockError),
86 #[error("admit expired at {exp}")]
88 Expired { exp: i64 },
89 #[error("admit names authority {got}, expected {expected}")]
91 WrongAuthority { expected: String, got: String },
92 #[error("no snapshot held for {kind}/{id}")]
94 NoSnapshot { kind: String, id: String },
95 #[error(transparent)]
97 Snapshot(#[from] SnapshotError),
98 #[error(transparent)]
100 Standing(#[from] StandingError),
101 #[error("{approver} does not hold the admit ability on {kind}/{id}")]
103 NotAnApprover { approver: PrincipalId, kind: String, id: String },
104 #[error("admit {jti} is revoked")]
106 Revoked { jti: String },
107 #[error("admission policy refuses admit {jti}: {reason}")]
111 PolicyRefused { jti: String, reason: AdmissionRefusal },
112 #[error(transparent)]
113 Store(#[from] StoreError),
114}
115
116impl From<VerifyError> for AdmitError {
117 fn from(e: VerifyError) -> Self {
118 AdmitError::Snapshot(SnapshotError::Verify(e))
119 }
120}
121
122#[derive(Debug, Clone, PartialEq, Eq)]
124pub struct Approval {
125 pub user: Option<UserId>,
128 pub relation: String,
130 pub policy: Option<AdmissionPolicy>,
134 pub epoch: u64,
137}
138
139#[async_trait::async_trait]
145pub trait AdmitAuthority: Send + Sync {
146 async fn approval_at(
147 &self,
148 approver: &PrincipalId,
149 approver_binding: Option<&str>,
150 kind: &str,
151 id: &str,
152 now: i64,
153 ) -> Result<Approval, AdmitError>;
154
155 async fn requester_user_at(&self, requester: &PeerKey, binding: &str, now: i64) -> Result<UserId, AdmitError>;
159}
160
161pub async fn verify_admit_with(
171 authority: &dyn AdmitAuthority,
172 trusted_authority: &str,
173 revocations: &dyn RevocationReader,
174 token: &str,
175 now: i64,
176) -> Result<VerifiedAdmit, AdmitError> {
177 let admit: Admit = verify_device_artifact(token)?;
178 admit.validate()?;
179 if admit.authority != trusted_authority {
180 return Err(AdmitError::WrongAuthority { expected: trusted_authority.to_owned(), got: admit.authority });
181 }
182 let lease = admit.lease.state_at(now);
183 if lease == LeaseState::Expired {
184 return Err(AdmitError::Expired { exp: admit.lease.exp().unwrap_or(now) });
185 }
186 if revocations.is_revoked(&admit.jti).await? {
187 return Err(AdmitError::Revoked { jti: admit.jti });
188 }
189 let approval = authority
190 .approval_at(&admit.approver, admit.approver_binding.as_deref(), &admit.kind, &admit.id, now)
191 .await?;
192 let requester_user = match &admit.requester_binding {
193 Some(binding) => {
194 let key = key_bytes(&admit.requester).ok_or(KnockError::NotAKey(admit.requester.clone()))?;
195 Some(authority.requester_user_at(&PeerKey::ed25519(key), binding, now).await?)
196 }
197 None => None,
198 };
199 let facts = AdmitFacts {
200 relation: &admit.relation,
201 path: admit.path(),
202 confirmation: Some(admit.confirmation),
203 requester_is_user: requester_user.is_some(),
204 };
205 if let Err(reason) = evaluate_admit(approval.policy.as_ref(), &facts, admit.iat, Some(LeaseRequest::Exact(admit.lease))) {
206 return Err(AdmitError::PolicyRefused { jti: admit.jti, reason });
207 }
208 Ok(VerifiedAdmit { admit, approval, requester_user, lease })
209}
210
211#[derive(Debug, Clone, PartialEq, Eq)]
213pub struct VerifiedAdmit {
214 pub admit: Admit,
215 pub approval: Approval,
216 pub requester_user: Option<UserId>,
219 pub lease: LeaseState,
221}
222
223pub struct IssuerAdmitAuthority<Rd> {
225 standing: StandingVerifier<Rd>,
226 snapshots: SnapshotVerifier<Rd>,
227 held: RwLock<BTreeMap<(String, String), (u64, String)>>,
229}
230
231impl<Rd> std::fmt::Debug for IssuerAdmitAuthority<Rd> {
232 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
233 f.debug_struct("IssuerAdmitAuthority").finish_non_exhaustive()
234 }
235}
236
237impl<Rd: RevocationReader> IssuerAdmitAuthority<Rd> {
238 pub fn new(standing: StandingVerifier<Rd>, snapshots: SnapshotVerifier<Rd>) -> Self {
242 Self {
243 standing,
244 snapshots,
245 held: RwLock::new(BTreeMap::new()),
246 }
247 }
248
249 pub fn standing(&self) -> &StandingVerifier<Rd> {
250 &self.standing
251 }
252
253 pub fn snapshots(&self) -> &SnapshotVerifier<Rd> {
254 &self.snapshots
255 }
256
257 pub async fn hold_snapshot(&self, token: &str) -> Result<u64, VerifyError> {
260 let snapshot = self.snapshots.adopt(token).await?;
261 let mut held = self.held.write().unwrap_or_else(PoisonError::into_inner);
262 let slot = held.entry((snapshot.kind, snapshot.id)).or_insert((0, String::new()));
263 if snapshot.epoch >= slot.0 {
264 *slot = (snapshot.epoch, token.to_owned());
265 }
266 Ok(slot.0)
267 }
268
269 fn held_token(&self, kind: &str, id: &str) -> Option<String> {
270 let held = self.held.read().unwrap_or_else(PoisonError::into_inner);
271 held.get(&(kind.to_owned(), id.to_owned())).map(|(_, t)| t.clone())
272 }
273
274 pub async fn verify_admit_at(&self, token: &str, now: i64) -> Result<VerifiedAdmit, AdmitError> {
279 verify_admit_with(self, self.snapshots.trust().issuer(), self.snapshots.revocations(), token, now).await
280 }
281
282 fn admit_relation(&self, snapshot: &VerifiedSnapshot, principal: &PrincipalId) -> Option<String> {
283 let admitters = &snapshot.admission_policy()?.admitters;
286 admitters.iter().find(|r| snapshot.holds(principal, r)).cloned()
287 }
288}
289
290#[async_trait::async_trait]
291impl<Rd: RevocationReader + Send + Sync> AdmitAuthority for IssuerAdmitAuthority<Rd> {
292 async fn approval_at(
293 &self,
294 approver: &PrincipalId,
295 approver_binding: Option<&str>,
296 kind: &str,
297 id: &str,
298 now: i64,
299 ) -> Result<Approval, AdmitError> {
300 let not_an_approver = || AdmitError::NotAnApprover {
301 approver: approver.clone(),
302 kind: kind.to_owned(),
303 id: id.to_owned(),
304 };
305 if approver.kind != PrincipalKind::Key {
306 return Err(not_an_approver());
307 }
308 let token = self.held_token(kind, id).ok_or_else(|| AdmitError::NoSnapshot {
309 kind: kind.to_owned(),
310 id: id.to_owned(),
311 })?;
312 let snapshot = self.snapshots.verify_snapshot_at(&token, now).await?;
313
314 let approval = |user, relation| Approval {
315 user,
316 relation,
317 policy: snapshot.admission_policy().cloned(),
318 epoch: snapshot.snapshot.epoch,
319 };
320 if let Some(relation) = self.admit_relation(&snapshot, approver) {
322 return Ok(approval(None, relation));
323 }
324 let Some(binding) = approver_binding else {
326 return Err(not_an_approver());
327 };
328 let key = key_bytes(approver).ok_or_else(not_an_approver)?;
329 let standing = self.standing.verify_standing_at(binding, &PeerKey::ed25519(key), now).await?;
330 let user = standing.binding.sub;
331 match self.admit_relation(&snapshot, &PrincipalId::from(&user)) {
332 Some(relation) => Ok(approval(Some(user), relation)),
333 None => Err(not_an_approver()),
334 }
335 }
336
337 async fn requester_user_at(&self, requester: &PeerKey, binding: &str, now: i64) -> Result<UserId, AdmitError> {
338 Ok(self.standing.verify_standing_at(binding, requester, now).await?.binding.sub)
339 }
340}
341
342#[cfg(test)]
343mod tests {
344 use super::*;
345 use crate::artifact::IssuerTrust;
346 use crate::jwks::{JwkKey, JwksDoc, KeySet};
347 use crate::key_set::KeySetVerifier;
348 use crate::revocation::{revocation_entry, test_revocation_key, ReplicatedRevocations};
349 use cheers_core::{
350 AdmissionMode, AdmissionPolicy, AdmitSource, Confirmation, DeviceId, Knock, KeyRole, Lease, ResourceRevocationKey, RevocationKey, RevocationSet, Revoked,
351 SetSnapshot, SignedArtifact, SnapshotMember, StandingBinding,
352 };
353 use pasetors::keys::{AsymmetricKeyPair, AsymmetricSecretKey, Generate};
354 use pasetors::version4::PublicToken;
355 use serde::Serialize;
356 use std::sync::Arc;
357
358 const ISS: &str = "https://cheers.test";
359 const IAT: i64 = 1_000;
360 const DAY: i64 = 24 * 60 * 60;
361
362 fn kp() -> AsymmetricKeyPair<V4> {
363 AsymmetricKeyPair::<V4>::generate().unwrap()
364 }
365
366 fn pub_bytes(k: &AsymmetricKeyPair<V4>) -> [u8; 32] {
367 k.public.as_bytes().try_into().unwrap()
368 }
369
370 fn key_of(k: &AsymmetricKeyPair<V4>) -> PrincipalId {
371 PrincipalId::from_public_key(&pub_bytes(k))
372 }
373
374 fn sign_with(secret: &AsymmetricSecretKey<V4>, kid: &str, payload: &impl Serialize, ia: &[u8]) -> String {
375 let body = serde_json::to_vec(payload).unwrap();
376 let footer = format!(r#"{{"kid":"{kid}"}}"#);
377 PublicToken::sign(secret, &body, Some(footer.as_bytes()), Some(ia)).unwrap()
378 }
379
380 fn member(principal: PrincipalId, relation: &str) -> SnapshotMember {
381 SnapshotMember { principal, relation: relation.into(), via: vec![("namespace".into(), "ed".into())], lease: None }
382 }
383
384 struct Rig {
385 issuer: AsymmetricKeyPair<V4>,
386 replica: Arc<ReplicatedRevocations>,
387 auth: IssuerAdmitAuthority<Arc<ReplicatedRevocations>>,
388 device: AsymmetricKeyPair<V4>,
390 binding: String,
391 }
392
393 impl Rig {
394 fn snapshot(&self, epoch: u64, members: Vec<SnapshotMember>) {
395 self.snapshot_with(epoch, members, Some(AdmissionPolicy::new(AdmissionMode::Knock, "guest")));
396 }
397
398 fn snapshot_with(&self, epoch: u64, members: Vec<SnapshotMember>, policy: Option<AdmissionPolicy>) {
400 self.snapshot_exact(epoch, members, policy.map(|p| p.with_admitters(["owner"])));
401 }
402
403 fn snapshot_exact(&self, epoch: u64, members: Vec<SnapshotMember>, policy: Option<AdmissionPolicy>) {
404 let s = SetSnapshot::new(
405 ISS,
406 "namespace",
407 "ed",
408 epoch,
409 members,
410 vec![ResourceRevocationKey {
411 kind: "namespace".into(),
412 id: "ed".into(),
413 key: test_revocation_key("namespace", "ed"),
414 }],
415 IAT,
416 Lease::new(IAT, IAT + 7 * DAY, None).unwrap(),
417 )
418 .with_policy(policy);
419 let t = sign_with(&self.issuer.secret, "iss-1", &s, SetSnapshot::IMPLICIT_ASSERTION);
420 pollster::block_on(self.auth.hold_snapshot(&t)).unwrap();
421 }
422
423 fn admit(&self, signer: &AsymmetricKeyPair<V4>, binding: Option<&str>, level: Confirmation) -> Admit {
424 Admit {
425 approver: key_of(signer),
426 approver_binding: binding.map(str::to_owned),
427 authority: ISS.into(),
428 requester: key_of(&kp()),
429 requester_binding: None,
430 kind: "namespace".into(),
431 id: "ed".into(),
432 relation: "guest".into(),
433 source: AdmitSource::Knock(artifact_hash("knock-token")),
434 confirmation: level,
435 epoch: 10,
436 jti: "admit-1".into(),
437 iat: IAT,
438 lease: Lease::new(IAT, IAT + DAY, Some(IAT + 7 * DAY)).unwrap(),
439 }
440 }
441
442 fn bind(&self, by: &AsymmetricSecretKey<V4>, user: &str, peer: &AsymmetricKeyPair<V4>) -> String {
444 let b = StandingBinding {
445 issuer: ISS.into(),
446 sub: UserId::new(user),
447 device: DeviceId::new(format!("node:{user}")),
448 peer_key: PeerKey::ed25519(pub_bytes(peer)),
449 seq: 1,
450 iat: IAT,
451 jti: format!("bind-{user}"),
452 lease: Lease::new(IAT, IAT + 7 * DAY, None).unwrap(),
453 };
454 sign_with(by, "iss-1", &b, StandingBinding::IMPLICIT_ASSERTION)
455 }
456
457 fn sign(&self, signer: &AsymmetricKeyPair<V4>, a: &Admit) -> String {
458 sign_with(&signer.secret, "dev", a, Admit::IMPLICIT_ASSERTION)
459 }
460
461 fn verify(&self, token: &str, now: i64) -> Result<VerifiedAdmit, AdmitError> {
462 pollster::block_on(self.auth.verify_admit_at(token, now))
463 }
464 }
465
466 fn rig() -> Rig {
467 let issuer = kp();
468 let doc = JwksDoc { keys: vec![JwkKey::ed25519("iss-1", &pub_bytes(&issuer), ISS, KeyRole::Issuer)] };
469 let trust = IssuerTrust::key_set(ISS, KeySetVerifier::from_key_set(KeySet::from_doc(doc).unwrap()));
470 let replica = Arc::new(ReplicatedRevocations::new(trust.clone()));
471 let auth = IssuerAdmitAuthority::new(
472 StandingVerifier::new(trust.clone(), replica.clone()),
473 SnapshotVerifier::new(trust, replica.clone()),
474 );
475 let device = kp();
476 let b = StandingBinding {
477 issuer: ISS.into(),
478 sub: UserId::new("alice"),
479 device: DeviceId::new("node:alice"),
480 peer_key: PeerKey::ed25519(pub_bytes(&device)),
481 seq: 1,
482 iat: IAT,
483 jti: "bind-1".into(),
484 lease: Lease::new(IAT, IAT + 7 * DAY, None).unwrap(),
485 };
486 let binding = sign_with(&issuer.secret, "iss-1", &b, StandingBinding::IMPLICIT_ASSERTION);
487 let r = Rig { issuer, replica, auth, device, binding };
488 r.snapshot(10, vec![member(PrincipalId::user("alice"), "owner")]);
489 r
490 }
491
492 #[test]
493 fn a_valid_chain_admits() {
494 let r = rig();
495 let a = r.admit(&r.device, Some(&r.binding), Confirmation::Compare);
496 let ok = r.verify(&r.sign(&r.device, &a), IAT + 1).unwrap();
497 assert_eq!(ok.admit, a);
498 assert_eq!(ok.approval.user, Some(UserId::new("alice")));
499 assert_eq!(ok.approval.relation, "owner");
500 assert_eq!(ok.lease, LeaseState::Current);
501 }
502
503 #[test]
504 fn a_key_with_its_own_issuer_tuple_admits_without_a_binding() {
505 let r = rig();
506 let k = kp();
507 r.snapshot(11, vec![member(key_of(&k), "owner")]);
508 let a = r.admit(&k, None, Confirmation::Accept);
509 assert_eq!(r.verify(&r.sign(&k, &a), IAT).unwrap().approval.user, None);
510 }
511
512 #[test]
513 fn a_demoted_approver_loses_unreconciled_admits() {
514 let r = rig();
515 let token = r.sign(&r.device, &r.admit(&r.device, Some(&r.binding), Confirmation::Accept));
516 assert!(r.verify(&token, IAT).is_ok());
517 r.snapshot(11, vec![member(PrincipalId::user("alice"), "guest")]);
518 assert!(matches!(r.verify(&token, IAT).unwrap_err(), AdmitError::NotAnApprover { .. }));
519 }
520
521 #[test]
522 fn a_two_hop_chain_is_refused() {
523 let r = rig();
524 let g = kp();
526 let mut first = r.admit(&r.device, Some(&r.binding), Confirmation::Accept);
527 first.requester = key_of(&g);
528 assert!(r.verify(&r.sign(&r.device, &first), IAT).is_ok());
529 let second = r.admit(&g, None, Confirmation::Accept);
530 let err = r.verify(&r.sign(&g, &second), IAT).unwrap_err();
531 assert!(matches!(err, AdmitError::NotAnApprover { .. }), "got {err:?}");
532 let borrowed = r.admit(&g, Some(&r.binding), Confirmation::Accept);
534 let err = r.verify(&r.sign(&g, &borrowed), IAT).unwrap_err();
535 assert!(matches!(err, AdmitError::Standing(StandingError::PeerKeyMismatch)), "got {err:?}");
536 }
537
538 #[test]
539 fn the_policy_held_now_decides() {
540 let r = rig();
541 let members = || vec![member(PrincipalId::user("alice"), "owner")];
542 let token = r.sign(&r.device, &r.admit(&r.device, Some(&r.binding), Confirmation::Compare));
543 assert!(r.verify(&token, IAT).is_ok());
544 let strict = AdmissionPolicy::new(AdmissionMode::Knock, "guest").with_min_confirmation("guest", Confirmation::Scan);
546 r.snapshot_with(11, members(), Some(strict));
547 let err = r.verify(&token, IAT).unwrap_err();
548 assert!(
549 matches!(err, AdmitError::PolicyRefused { reason: AdmissionRefusal::BelowConfirmation { .. }, .. }),
550 "got {err:?}"
551 );
552 r.snapshot_with(12, members(), None);
556 assert!(matches!(r.verify(&token, IAT).unwrap_err(), AdmitError::NotAnApprover { .. }));
557 r.snapshot_with(13, members(), Some(AdmissionPolicy::new(AdmissionMode::Users, "guest")));
558 assert!(matches!(r.verify(&token, IAT).unwrap_err(), AdmitError::PolicyRefused { .. }));
559 r.snapshot(14, members());
561 assert!(r.verify(&token, IAT).is_ok());
562 }
563
564 #[test]
565 fn the_admit_ability_comes_from_the_held_policy() {
566 let r = rig();
567 let members = || vec![member(PrincipalId::user("alice"), "owner")];
568 let token = r.sign(&r.device, &r.admit(&r.device, Some(&r.binding), Confirmation::Accept));
569 r.snapshot_exact(20, members(), Some(AdmissionPolicy::new(AdmissionMode::Knock, "guest")));
571 assert!(matches!(r.verify(&token, IAT).unwrap_err(), AdmitError::NotAnApprover { .. }));
572 r.snapshot(21, members());
574 assert!(r.verify(&token, IAT).is_ok());
575 }
576
577 #[test]
578 fn a_lease_past_the_path_maximum_is_refused() {
579 let r = rig();
580 let mut a = r.admit(&r.device, Some(&r.binding), Confirmation::Accept);
581 a.lease = Lease::new(IAT, IAT + DAY, Some(IAT + 8 * DAY)).unwrap();
582 let err = r.verify(&r.sign(&r.device, &a), IAT).unwrap_err();
583 assert!(matches!(err, AdmitError::PolicyRefused { reason: AdmissionRefusal::LeaseOverMax { .. }, .. }), "got {err:?}");
584 }
585
586 #[test]
587 fn a_lease_past_its_midpoint_is_refused() {
588 let r = rig();
589 let mut v = serde_json::to_value(r.admit(&r.device, Some(&r.binding), Confirmation::Accept)).unwrap();
590 v["refresh_after"] = (IAT + 4 * DAY).into();
591 let forged: Admit = serde_json::from_value(v).unwrap();
592 let err = r.verify(&r.sign(&r.device, &forged), IAT).unwrap_err();
593 assert!(matches!(err, AdmitError::Invalid(KnockError::Lease(_))), "got {err:?}");
594 }
595
596 #[test]
597 fn lease_state_through_exp_then_refused() {
598 let r = rig();
599 let token = r.sign(&r.device, &r.admit(&r.device, Some(&r.binding), Confirmation::Accept));
600 let (ra, exp) = (IAT + DAY, IAT + 7 * DAY);
601 assert_eq!(r.verify(&token, ra - 1).unwrap().lease, LeaseState::Current);
602 assert_eq!(r.verify(&token, ra).unwrap().lease, LeaseState::Warning { exp: Some(exp) });
603 assert_eq!(r.verify(&token, exp - 1).unwrap().lease, LeaseState::Warning { exp: Some(exp) });
604 assert!(matches!(r.verify(&token, exp).unwrap_err(), AdmitError::Expired { .. }));
605 }
606
607 #[test]
608 fn tampered_and_wrong_assertion_artifacts_are_refused() {
609 let r = rig();
610 let a = r.admit(&r.device, Some(&r.binding), Confirmation::Accept);
611 let other = kp();
613 assert!(matches!(r.verify(&r.sign(&other, &a), IAT).unwrap_err(), AdmitError::Signature(_)));
614 let token = r.sign(&r.device, &a);
616 let mut bytes = token.into_bytes();
617 let i = bytes.len() - 10;
618 bytes[i] = if bytes[i] == b'A' { b'B' } else { b'A' };
619 assert!(r.verify(&String::from_utf8(bytes).unwrap(), IAT).is_err());
620 let as_knock = sign_with(&r.device.secret, "dev", &a, Knock::IMPLICIT_ASSERTION);
622 assert!(matches!(r.verify(&as_knock, IAT).unwrap_err(), AdmitError::Signature(_)));
623 }
624
625 fn revoke(r: &Rig, jti: &str) {
627 let entries = [Revoked::Jti { jti: jti.into(), exp: Some(IAT + 7 * DAY) }]
628 .iter()
629 .map(|e| revocation_entry(e, test_revocation_key))
630 .collect();
631 let set = RevocationSet::new(ISS, 5, entries);
632 let t = sign_with(&r.issuer.secret, "iss-1", &set, RevocationSet::IMPLICIT_ASSERTION);
633 pollster::block_on(r.replica.adopt(&t)).unwrap();
634 }
635
636 #[test]
637 fn a_revoked_admit_jti_is_refused() {
638 let r = rig();
639 let token = r.sign(&r.device, &r.admit(&r.device, Some(&r.binding), Confirmation::Accept));
640 revoke(&r, "admit-1");
641 assert!(matches!(r.verify(&token, IAT).unwrap_err(), AdmitError::Revoked { .. }));
642 }
643
644 #[test]
648 fn refusals_keep_the_chain_order() {
649 let r = rig();
650 let k = kp();
651 let users = |epoch, alice: &str| {
652 let policy = AdmissionPolicy::new(AdmissionMode::Users, "guest");
653 r.snapshot_with(epoch, vec![member(PrincipalId::user("alice"), alice)], Some(policy));
654 };
655 users(11, "guest");
656 revoke(&r, "admit-1");
657 let mut a = r.admit(&r.device, Some(&r.binding), Confirmation::Accept);
658 a.authority = "https://rogue.test".into();
659 a.requester = key_of(&k);
660 a.requester_binding = Some(r.bind(&r.issuer.secret, "bob", &kp()));
661 a.lease = Lease::new(IAT, IAT + DAY, Some(IAT + 8 * DAY)).unwrap();
662 let exp = IAT + 8 * DAY;
663 let refuse = |a: &Admit, now| r.verify(&r.sign(&r.device, a), now).unwrap_err();
664
665 let err = refuse(&a, exp);
666 assert!(matches!(err, AdmitError::WrongAuthority { .. }), "got {err:?}");
667 a.authority = ISS.into();
668 let err = refuse(&a, exp);
669 assert!(matches!(err, AdmitError::Expired { .. }), "got {err:?}");
670 let err = refuse(&a, IAT);
671 assert!(matches!(err, AdmitError::Revoked { .. }), "got {err:?}");
672 a.jti = "admit-2".into();
673 let err = refuse(&a, IAT);
674 assert!(matches!(err, AdmitError::NotAnApprover { .. }), "got {err:?}");
675 users(12, "owner");
676 let err = refuse(&a, IAT);
677 assert!(matches!(err, AdmitError::Standing(StandingError::PeerKeyMismatch)), "got {err:?}");
678 a.requester_binding = Some(r.bind(&r.issuer.secret, "bob", &k));
679 let err = refuse(&a, IAT);
680 assert!(matches!(err, AdmitError::PolicyRefused { reason: AdmissionRefusal::LeaseOverMax { .. }, .. }), "got {err:?}");
681 a.lease = Lease::new(IAT, IAT + DAY, Some(IAT + 7 * DAY)).unwrap();
682 let ok = r.verify(&r.sign(&r.device, &a), IAT).unwrap();
683 assert_eq!(ok.requester_user, Some(UserId::new("bob")));
684 assert_eq!(ok.approval.epoch, 12);
685 }
686
687 #[derive(Clone)]
691 struct Roster {
692 root: String,
693 members: BTreeMap<PrincipalId, String>,
694 policy: Option<AdmissionPolicy>,
695 epoch: u64,
696 }
697
698 #[async_trait::async_trait]
699 impl AdmitAuthority for Roster {
700 async fn approval_at(
701 &self,
702 approver: &PrincipalId,
703 _approver_binding: Option<&str>,
704 kind: &str,
705 id: &str,
706 _now: i64,
707 ) -> Result<Approval, AdmitError> {
708 let not_an_approver = || AdmitError::NotAnApprover { approver: approver.clone(), kind: kind.into(), id: id.into() };
709 if (kind, id) != ("room", self.root.as_str()) {
710 return Err(not_an_approver());
711 }
712 let relation = self.members.get(approver).filter(|r| *r == "enroll").cloned().ok_or_else(not_an_approver)?;
713 Ok(Approval { user: None, relation, policy: self.policy.clone(), epoch: self.epoch })
714 }
715
716 async fn requester_user_at(&self, _requester: &PeerKey, _binding: &str, _now: i64) -> Result<UserId, AdmitError> {
717 Err(AdmitError::Standing(StandingError::PeerKeyMismatch))
718 }
719 }
720
721 struct Jtis(std::collections::BTreeSet<String>);
723
724 #[async_trait::async_trait]
725 impl RevocationReader for Jtis {
726 async fn is_revoked(&self, jti: &str) -> Result<bool, StoreError> {
727 Ok(self.0.contains(jti))
728 }
729
730 async fn is_device_revoked(&self, _device: &DeviceId, _seq: u64) -> Result<bool, StoreError> {
731 Ok(false)
732 }
733
734 async fn is_membership_revoked(
735 &self,
736 _key: &RevocationKey,
737 _kind: &str,
738 _id: &str,
739 _principal: &PrincipalId,
740 _snapshot_epoch: u64,
741 ) -> Result<bool, StoreError> {
742 Ok(false)
743 }
744 }
745
746 #[test]
747 fn a_non_issuer_root_verifies_an_offline_admit() {
748 let (root, enroller, stranger) = (kp(), kp(), kp());
749 let room = URL_SAFE_NO_PAD.encode(pub_bytes(&root));
750 let authority = format!("room:{room}");
751 let roster = Roster {
752 root: room.clone(),
753 members: BTreeMap::from([(key_of(&enroller), "enroll".to_owned()), (key_of(&stranger), "guest".to_owned())]),
754 policy: Some(AdmissionPolicy::new(AdmissionMode::Knock, "guest")),
755 epoch: 3,
756 };
757 let none = Jtis(Default::default());
758 let admit = |signer: &AsymmetricKeyPair<V4>, authority: &str| {
759 let a = Admit {
760 approver: key_of(signer),
761 approver_binding: None,
762 authority: authority.into(),
763 requester: key_of(&kp()),
764 requester_binding: None,
765 kind: "room".into(),
766 id: room.clone(),
767 relation: "guest".into(),
768 source: AdmitSource::Knock(artifact_hash("knock-token")),
769 confirmation: Confirmation::Compare,
770 epoch: 3,
771 jti: "room-admit-1".into(),
772 iat: IAT,
773 lease: Lease::new(IAT, IAT + DAY, Some(IAT + 7 * DAY)).unwrap(),
774 };
775 let token = sign_with(&signer.secret, "dev", &a, Admit::IMPLICIT_ASSERTION);
776 (a, token)
777 };
778 let verify = |roster: &Roster, revocations: &Jtis, token: &str| {
779 pollster::block_on(verify_admit_with(roster, &authority, revocations, token, IAT + 1))
780 };
781
782 let (a, token) = admit(&enroller, &authority);
783 let ok = verify(&roster, &none, &token).unwrap();
784 assert_eq!(ok.admit, a);
785 assert_eq!(ok.approval, Approval { user: None, relation: "enroll".into(), policy: roster.policy.clone(), epoch: 3 });
786 assert_eq!(ok.requester_user, None);
787 assert_eq!(ok.lease, LeaseState::Current);
788
789 let (_, token) = admit(&enroller, ISS);
791 let err = verify(&roster, &none, &token).unwrap_err();
792 assert!(matches!(&err, AdmitError::WrongAuthority { expected, .. } if *expected == authority), "got {err:?}");
793 let (_, token) = admit(&stranger, &authority);
795 let err = verify(&roster, &none, &token).unwrap_err();
796 assert!(matches!(err, AdmitError::NotAnApprover { .. }), "got {err:?}");
797 let (_, token) = admit(&enroller, &authority);
799 let revoked = Jtis(["room-admit-1".to_owned()].into());
800 let err = verify(&roster, &revoked, &token).unwrap_err();
801 assert!(matches!(err, AdmitError::Revoked { .. }), "got {err:?}");
802 let users = Roster { policy: Some(AdmissionPolicy::new(AdmissionMode::Users, "guest")), ..roster.clone() };
804 let err = verify(&users, &none, &token).unwrap_err();
805 assert!(matches!(err, AdmitError::PolicyRefused { .. }), "got {err:?}");
806 }
807
808 fn users_mode_admit(r: &Rig, k: &AsymmetricKeyPair<V4>, binding: String) -> String {
811 r.snapshot_with(
812 11,
813 vec![member(PrincipalId::user("alice"), "owner")],
814 Some(AdmissionPolicy::new(AdmissionMode::Users, "guest")),
815 );
816 let mut a = r.admit(&r.device, Some(&r.binding), Confirmation::Accept);
817 a.requester = key_of(k);
818 a.requester_binding = Some(binding);
819 r.sign(&r.device, &a)
820 }
821
822 #[test]
823 fn users_mode_admits_a_requester_with_its_own_binding() {
824 let r = rig();
825 let k = kp();
826 let token = users_mode_admit(&r, &k, r.bind(&r.issuer.secret, "bob", &k));
827 let ok = r.verify(&token, IAT).unwrap();
828 assert_eq!(ok.requester_user, Some(UserId::new("bob")));
829 assert_eq!(ok.admit.requester, key_of(&k));
830 }
831
832 #[test]
833 fn a_requester_binding_for_another_key_is_refused() {
834 let r = rig();
835 let (k, other) = (kp(), kp());
836 let token = users_mode_admit(&r, &k, r.bind(&r.issuer.secret, "bob", &other));
837 let err = r.verify(&token, IAT).unwrap_err();
838 assert!(matches!(err, AdmitError::Standing(StandingError::PeerKeyMismatch)), "got {err:?}");
839 }
840
841 #[test]
842 fn a_requester_binding_from_an_untrusted_issuer_is_refused() {
843 let r = rig();
844 let k = kp();
845 let rogue = kp();
846 let token = users_mode_admit(&r, &k, r.bind(&rogue.secret, "bob", &k));
847 let err = r.verify(&token, IAT).unwrap_err();
848 assert!(matches!(err, AdmitError::Standing(StandingError::Verify(_))), "got {err:?}");
849 }
850
851 #[test]
852 fn a_knock_verifies_under_its_requester_key() {
853 let k = kp();
854 let knock = Knock {
855 requester: key_of(&k),
856 kind: "namespace".into(),
857 id: "ed".into(),
858 relation: "guest".into(),
859 nonce: "n".into(),
860 label: "Ann's phone".into(),
861 standing: None,
862 renews: Some("admit-1".into()),
863 iat: IAT,
864 };
865 let t = sign_with(&k.secret, "dev", &knock, Knock::IMPLICIT_ASSERTION);
866 assert_eq!(verify_device_artifact::<Knock>(&t).unwrap(), knock);
867 assert!(verify_device_artifact::<Admit>(&t).is_err());
868 }
869}