1use std::collections::{BTreeMap, HashMap};
40use std::sync::{PoisonError, RwLock};
41
42use serde::{Deserialize, Serialize};
43
44use cheers_core::{LeaseError, LeaseState, PrincipalId, SetSnapshot, SnapshotMember, StoreError};
45
46use crate::artifact::IssuerTrust;
47use crate::key_set::VerifyError;
48use crate::revocation::RevocationReader;
49use crate::standing::ForeignLedger;
50
51#[derive(Debug, thiserror::Error)]
54pub enum SnapshotError {
55 #[error("membership snapshot refused: {0}")]
59 Verify(#[from] VerifyError),
60
61 #[error("snapshot of {kind}/{id} is superseded: epoch {offered} is below {newest}")]
63 Superseded { kind: String, id: String, newest: u64, offered: u64 },
64
65 #[error("snapshot carries no revocation key for {kind}/{id}")]
69 MissingRevocationKey { kind: String, id: String },
70
71 #[error("membership snapshot refused: {0}")]
73 Lease(#[from] LeaseError),
74
75 #[error(transparent)]
77 Store(#[from] StoreError),
78}
79
80#[derive(Debug, Clone, PartialEq, Eq)]
82pub struct VerifiedSnapshot {
83 pub snapshot: SetSnapshot,
85 pub admitted: Vec<SnapshotMember>,
88 pub lease: LeaseState,
92}
93
94impl VerifiedSnapshot {
95 pub fn holds(&self, principal: &PrincipalId, relation: &str) -> bool {
99 self.admitted.iter().any(|m| &m.principal == principal && m.relation == relation)
100 }
101
102 pub fn member_lease(&self, principal: &PrincipalId, relation: &str, now: i64) -> Option<LeaseState> {
106 self.admitted
107 .iter()
108 .find(|m| &m.principal == principal && m.relation == relation)
109 .and_then(|m| m.lease)
110 .map(|l| l.state_at(now))
111 }
112
113 pub fn admission_policy(&self) -> Option<&cheers_core::AdmissionPolicy> {
117 self.snapshot.policy.as_ref()
118 }
119}
120
121#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
124pub struct SnapshotLedgerDoc {
125 pub issuer: String,
126 pub newest: BTreeMap<String, BTreeMap<String, u64>>,
127}
128
129#[derive(Debug)]
135pub struct SnapshotLedger {
136 issuer: String,
137 newest: RwLock<HashMap<(String, String), u64>>,
138}
139
140impl SnapshotLedger {
141 pub fn new(issuer: impl Into<String>) -> Self {
142 Self {
143 issuer: issuer.into(),
144 newest: RwLock::new(HashMap::new()),
145 }
146 }
147
148 pub fn issuer(&self) -> &str {
149 &self.issuer
150 }
151
152 pub fn newest(&self, kind: &str, id: &str) -> Option<u64> {
154 self.newest
155 .read()
156 .unwrap_or_else(PoisonError::into_inner)
157 .get(&(kind.to_owned(), id.to_owned()))
158 .copied()
159 }
160
161 pub fn observe(&self, kind: &str, id: &str, epoch: u64) -> u64 {
164 let mut newest = self.newest.write().unwrap_or_else(PoisonError::into_inner);
165 let held = newest.entry((kind.to_owned(), id.to_owned())).or_insert(epoch);
166 *held = (*held).max(epoch);
167 *held
168 }
169
170 pub fn export(&self) -> SnapshotLedgerDoc {
172 let mut out: BTreeMap<String, BTreeMap<String, u64>> = BTreeMap::new();
173 for ((kind, id), epoch) in self.newest.read().unwrap_or_else(PoisonError::into_inner).iter() {
174 out.entry(kind.clone()).or_default().insert(id.clone(), *epoch);
175 }
176 SnapshotLedgerDoc {
177 issuer: self.issuer.clone(),
178 newest: out,
179 }
180 }
181
182 pub fn import(&self, doc: &SnapshotLedgerDoc) -> Result<(), ForeignLedger> {
184 if doc.issuer != self.issuer {
185 return Err(ForeignLedger {
186 expected: self.issuer.clone(),
187 got: doc.issuer.clone(),
188 });
189 }
190 for (kind, ids) in &doc.newest {
191 for (id, epoch) in ids {
192 self.observe(kind, id, *epoch);
193 }
194 }
195 Ok(())
196 }
197}
198
199pub struct SnapshotVerifier<Rd> {
202 trust: IssuerTrust,
203 revoked: Rd,
204 ledger: SnapshotLedger,
205}
206
207impl<Rd> std::fmt::Debug for SnapshotVerifier<Rd> {
208 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
209 f.debug_struct("SnapshotVerifier")
210 .field("trust", &self.trust)
211 .field("ledger", &self.ledger)
212 .finish_non_exhaustive()
213 }
214}
215
216impl<Rd: RevocationReader> SnapshotVerifier<Rd> {
217 pub fn new(trust: IssuerTrust, revoked: Rd) -> Self {
220 let ledger = SnapshotLedger::new(trust.issuer());
221 Self { trust, revoked, ledger }
222 }
223
224 pub fn trust(&self) -> &IssuerTrust {
225 &self.trust
226 }
227
228 pub fn revocations(&self) -> &Rd {
229 &self.revoked
230 }
231
232 pub fn ledger(&self) -> &SnapshotLedger {
233 &self.ledger
234 }
235
236 pub async fn adopt(&self, token: &str) -> Result<SetSnapshot, VerifyError> {
240 let snapshot: SetSnapshot = self.trust.verify(token).await?;
241 self.ledger.observe(&snapshot.kind, &snapshot.id, snapshot.epoch);
242 Ok(snapshot)
243 }
244
245 pub async fn verify_snapshot_at(&self, token: &str, now: i64) -> Result<VerifiedSnapshot, SnapshotError> {
253 let snapshot: SetSnapshot = self.trust.verify(token).await?;
254 snapshot.lease.validate(snapshot.iat)?;
255 let newest = self.ledger.observe(&snapshot.kind, &snapshot.id, snapshot.epoch);
256 if newest > snapshot.epoch {
257 return Err(SnapshotError::Superseded {
258 kind: snapshot.kind,
259 id: snapshot.id,
260 newest,
261 offered: snapshot.epoch,
262 });
263 }
264 let mut admitted = Vec::with_capacity(snapshot.members.len());
265 for member in &snapshot.members {
266 if member.lease.is_some_and(|l| l.state_at(now) == LeaseState::Expired) {
269 continue;
270 }
271 if !self.is_masked(&snapshot, member).await? {
272 admitted.push(member.clone());
273 }
274 }
275 let lease = snapshot.lease.state_at(now);
276 Ok(VerifiedSnapshot { snapshot, admitted, lease })
277 }
278
279 async fn is_masked(&self, snapshot: &SetSnapshot, member: &SnapshotMember) -> Result<bool, SnapshotError> {
282 for (kind, id) in &member.via {
283 let key = snapshot.revocation_key(kind, id).ok_or_else(|| SnapshotError::MissingRevocationKey {
284 kind: kind.clone(),
285 id: id.clone(),
286 })?;
287 if !self.revoked.is_membership_revoked(key, kind, id, &member.principal, snapshot.epoch).await? {
288 return Ok(false);
289 }
290 }
291 Ok(true)
292 }
293}
294
295#[cfg(test)]
296mod tests {
297 use super::*;
298 use cheers_core::{Lease, UserId};
299 use crate::jwks::{JwkKey, JwksDoc, KeySet};
300 use crate::key_set::KeySetVerifier;
301 use crate::public_verifier::PasetoV4PublicVerifier;
302 use crate::revocation::{revocation_entry, test_revocation_key, ReplicatedRevocations};
303 use crate::standing::{StandingError, StandingVerifier};
304 use cheers_core::{
305 Claims, DeviceBinding, DeviceId, KeyRole, McpClaims, PeerKey, PrincipalId, ResourceRevocationKey, RevocationSet, Revoked,
306 SignedArtifact, StandingBinding, TokenVerifier,
307 };
308 use pasetors::keys::{AsymmetricKeyPair, AsymmetricSecretKey, Generate};
309 use pasetors::version4::{PublicToken, V4};
310 use std::sync::Arc;
311
312 const ISS: &str = "https://cheers.test";
313 const IAT: i64 = 1_000;
314 const YEARS_LATER: i64 = IAT + 10 * 365 * 24 * 60 * 60;
316
317 fn kp() -> AsymmetricKeyPair<V4> {
318 AsymmetricKeyPair::<V4>::generate().unwrap()
319 }
320
321 fn pub_bytes(k: &AsymmetricKeyPair<V4>) -> [u8; 32] {
322 k.public.as_bytes().try_into().unwrap()
323 }
324
325 fn sign_with(secret: &AsymmetricSecretKey<V4>, kid: &str, payload: &impl Serialize, ia: Option<&[u8]>) -> String {
326 let body = serde_json::to_vec(payload).unwrap();
327 let footer = format!(r#"{{"kid":"{kid}"}}"#);
328 PublicToken::sign(secret, &body, Some(footer.as_bytes()), ia).unwrap()
329 }
330
331 fn member(user: &str, relation: &str, via: &[&str]) -> SnapshotMember {
332 SnapshotMember {
333 principal: PrincipalId::user(user),
334 relation: relation.into(),
335 via: via.iter().map(|i| ("namespace".to_owned(), (*i).to_owned())).collect(),
336 lease: None,
337 }
338 }
339
340 fn ed(epoch: u64) -> SetSnapshot {
343 SetSnapshot::new(
344 ISS,
345 "namespace",
346 "ed",
347 epoch,
348 vec![
349 member("alice", "guest", &["museum"]),
350 member("bob", "guest", &["ed", "museum"]),
351 member("bob", "member", &["ed"]),
352 ],
353 ["ed", "museum"]
354 .map(|id| ResourceRevocationKey {
355 kind: "namespace".into(),
356 id: id.into(),
357 key: test_revocation_key("namespace", id),
358 })
359 .to_vec(),
360 IAT,
361 Lease::new(IAT, IAT + 7 * 24 * 60 * 60, None).unwrap(),
362 )
363 }
364
365 struct Rig {
366 issuer: AsymmetricKeyPair<V4>,
367 assertion: AsymmetricKeyPair<V4>,
368 doc: JwksDoc,
369 replica: Arc<ReplicatedRevocations>,
370 edge: SnapshotVerifier<Arc<ReplicatedRevocations>>,
371 }
372
373 impl Rig {
374 fn sign(&self, s: &SetSnapshot) -> String {
375 sign_with(&self.issuer.secret, "iss-1", s, Some(SetSnapshot::IMPLICIT_ASSERTION))
376 }
377
378 fn revoke(&self, epoch: u64, revoked: Vec<Revoked>) {
379 let entries = revoked.iter().map(|e| revocation_entry(e, test_revocation_key)).collect();
380 let set = RevocationSet::new(ISS, epoch, entries);
381 let token = sign_with(&self.issuer.secret, "iss-1", &set, Some(RevocationSet::IMPLICIT_ASSERTION));
382 pollster::block_on(self.replica.adopt(&token)).unwrap();
383 }
384
385 fn verify(&self, token: &str, now: i64) -> Result<VerifiedSnapshot, SnapshotError> {
386 pollster::block_on(self.edge.verify_snapshot_at(token, now))
387 }
388
389 fn admitted(&self, token: &str) -> Vec<String> {
390 let ok = self.verify(token, IAT).unwrap();
391 ok.admitted.iter().map(|m| format!("{}#{}", m.principal.id, m.relation)).collect()
392 }
393 }
394
395 fn rig() -> Rig {
396 let (issuer, assertion) = (kp(), kp());
397 let doc = JwksDoc {
398 keys: vec![
399 JwkKey::ed25519("iss-1", &pub_bytes(&issuer), ISS, KeyRole::Issuer),
400 JwkKey::ed25519("asr-1", &pub_bytes(&assertion), "svc:issues", KeyRole::Assertion),
401 ],
402 };
403 let keys = KeySetVerifier::from_key_set(KeySet::from_doc(doc.clone()).unwrap());
404 let trust = IssuerTrust::key_set(ISS, keys);
405 let replica = Arc::new(ReplicatedRevocations::new(trust.clone()));
406 Rig {
407 edge: SnapshotVerifier::new(trust, replica.clone()),
408 replica,
409 issuer,
410 assertion,
411 doc,
412 }
413 }
414
415 #[test]
416 fn a_lease_past_its_midpoint_is_refused() {
417 let r = rig();
418 let mut v = serde_json::to_value(ed(10)).unwrap();
419 v["refresh_after"] = (IAT + 600).into();
420 v["exp"] = (IAT + 1_000).into();
421 let forged: SetSnapshot = serde_json::from_value(v).unwrap();
422 let err = r.verify(&r.sign(&forged), IAT).unwrap_err();
423 assert!(matches!(err, SnapshotError::Lease(_)), "got {err:?}");
424 }
425
426 #[test]
427 fn a_snapshot_verifies_years_after_iat_and_past_refresh_after() {
428 let r = rig();
429 let s = ed(10);
430 let token = r.sign(&s);
431 let ra = s.lease.refresh_after();
432 let ok = r.verify(&token, ra - 1).unwrap();
433 assert_eq!(ok.lease, LeaseState::Current);
434 assert_eq!(ok.snapshot, s);
435 assert_eq!(ok.admitted, s.members);
436 assert_eq!(r.verify(&token, ra).unwrap().lease, LeaseState::Warning { exp: None });
437 let later = r.verify(&token, YEARS_LATER).unwrap();
438 assert_eq!(later.lease, LeaseState::Warning { exp: None });
439 assert!(later.holds(&PrincipalId::user("bob"), "member"));
440 assert!(!later.holds(&PrincipalId::user("alice"), "member"));
441 }
442
443 #[test]
444 fn a_key_principal_is_held_and_masked_like_a_user() {
445 let r = rig();
446 let k = PrincipalId::from_public_key(&[5; 32]);
447 let mut s = ed(10);
448 s.members.push(SnapshotMember { principal: k.clone(), relation: "guest".into(), via: vec![("namespace".into(), "ed".into())], lease: None });
449 let s = SetSnapshot::new(ISS, "namespace", "ed", 10, s.members, s.revocation_keys, s.iat, s.lease);
450 let token = r.sign(&s);
451 let ok = r.verify(&token, IAT).unwrap();
452 assert!(ok.holds(&k, "guest"));
453 assert!(!ok.holds(&PrincipalId::user(k.id.clone()), "guest"));
455 r.revoke(1, vec![Revoked::membership("namespace", "ed", PrincipalId::user(k.id.clone()), 20)]);
458 assert!(r.verify(&token, IAT).unwrap().holds(&k, "guest"));
459 r.revoke(2, vec![Revoked::membership("namespace", "ed", k.clone(), 20)]);
460 assert!(!r.verify(&token, IAT).unwrap().holds(&k, "guest"));
461 }
462
463 #[test]
464 fn a_higher_epoch_supersedes_and_nothing_lower_unsupersedes() {
465 let r = rig();
466 let old = r.sign(&ed(10));
467 let new = r.sign(&ed(12));
468 r.verify(&old, IAT).unwrap();
469 r.verify(&old, IAT).unwrap();
471 r.verify(&new, IAT).unwrap();
472 match r.verify(&old, IAT) {
473 Err(SnapshotError::Superseded { kind, id, newest: 12, offered: 10 }) => {
474 assert_eq!((kind.as_str(), id.as_str()), ("namespace", "ed"));
475 }
476 other => panic!("expected Superseded, got {other:?}"),
477 }
478 pollster::block_on(r.edge.adopt(&r.sign(&ed(15)))).unwrap();
480 pollster::block_on(r.edge.adopt(&r.sign(&ed(11)))).unwrap();
481 assert_eq!(r.edge.ledger().newest("namespace", "ed"), Some(15));
482 assert!(matches!(r.verify(&new, IAT), Err(SnapshotError::Superseded { newest: 15, .. })));
483 let stale = SnapshotLedgerDoc {
485 issuer: ISS.into(),
486 newest: [("namespace".to_owned(), [("ed".to_owned(), 3)].into_iter().collect())].into_iter().collect(),
487 };
488 r.edge.ledger().import(&stale).unwrap();
489 assert_eq!(r.edge.ledger().newest("namespace", "ed"), Some(15));
490 let mut other = ed(1);
492 other.id = "band".into();
493 r.verify(&r.sign(&other), IAT).unwrap();
494 }
495
496 #[test]
497 fn an_older_epoch_with_a_looser_policy_is_refused() {
498 use cheers_core::{evaluate_admit, AdmissionMode, AdmissionPath, AdmissionPolicy, AdmitFacts, Confirmation};
499 let r = rig();
500 let open = r.sign(&ed(10).with_policy(Some(AdmissionPolicy::new(AdmissionMode::Open, "guest"))));
501 let closed = r.sign(&ed(12).with_policy(Some(AdmissionPolicy::new(AdmissionMode::Closed, "guest"))));
502 let admit = AdmitFacts { relation: "guest", path: AdmissionPath::Knock, confirmation: Some(Confirmation::Accept), requester_is_user: false };
503 let held = r.verify(&open, IAT).unwrap();
504 assert!(evaluate_admit(held.admission_policy(), &admit, IAT, None).is_ok());
505 let held = r.verify(&closed, IAT).unwrap();
506 assert!(evaluate_admit(held.admission_policy(), &admit, IAT, None).is_err());
507 assert!(matches!(r.verify(&open, IAT), Err(SnapshotError::Superseded { newest: 12, offered: 10, .. })));
509 let bare = r.verify(&r.sign(&ed(13)), IAT).unwrap();
511 assert_eq!(bare.admission_policy(), None);
512 assert!(evaluate_admit(bare.admission_policy(), &admit, IAT, None).is_err());
513 }
514
515 #[test]
516 fn a_leased_guest_warns_then_drops_at_exp() {
517 let r = rig();
518 let k = PrincipalId::from_public_key(&[6; 32]);
519 let mut s = ed(10);
520 let lease = Lease::new(IAT, IAT + 100, Some(IAT + 300)).unwrap();
521 s.members.push(SnapshotMember { principal: k.clone(), relation: "guest".into(), via: vec![("namespace".into(), "ed".into())], lease: Some(lease) });
522 let s = SetSnapshot::new(ISS, "namespace", "ed", 10, s.members, s.revocation_keys, s.iat, s.lease);
523 let token = r.sign(&s);
524 let ok = r.verify(&token, IAT).unwrap();
525 assert_eq!(ok.member_lease(&k, "guest", IAT), Some(LeaseState::Current));
526 assert_eq!(ok.member_lease(&k, "guest", IAT + 100), Some(LeaseState::Warning { exp: Some(IAT + 300) }));
527 assert_eq!(ok.member_lease(&PrincipalId::user("bob"), "member", IAT), None);
528 let late = r.verify(&token, IAT + 300).unwrap();
529 assert!(!late.holds(&k, "guest"));
530 assert!(late.holds(&PrincipalId::user("bob"), "member"));
531 }
532
533 #[test]
534 fn a_membership_revoked_at_v_masks_below_v_only() {
535 let r = rig();
536 r.revoke(1, vec![Revoked::membership("namespace", "ed", PrincipalId::user("bob"), 20)]);
538 assert_eq!(r.admitted(&r.sign(&ed(19))), ["alice#guest", "bob#guest"]);
541 assert_eq!(r.admitted(&r.sign(&ed(20))), ["alice#guest", "bob#guest", "bob#member"]);
544 assert_eq!(r.admitted(&r.sign(&ed(30))), ["alice#guest", "bob#guest", "bob#member"]);
545 }
546
547 #[test]
548 fn an_entry_is_dropped_only_when_every_via_is_revoked() {
549 let r = rig();
550 r.revoke(
552 1,
553 vec![
554 Revoked::membership("namespace", "museum", PrincipalId::user("alice"), 20),
555 Revoked::membership("namespace", "museum", PrincipalId::user("bob"), 20),
556 ],
557 );
558 let held = r.sign(&ed(10));
559 assert_eq!(r.admitted(&held), ["bob#guest", "bob#member"]);
562 r.revoke(2, vec![
564 Revoked::membership("namespace", "museum", PrincipalId::user("alice"), 20),
565 Revoked::membership("namespace", "museum", PrincipalId::user("bob"), 20),
566 Revoked::membership("namespace", "ed", PrincipalId::user("bob"), 25),
567 ]);
568 assert!(r.admitted(&held).is_empty());
569 let r = rig();
571 r.revoke(1, vec![
572 Revoked::membership("namespace", "museum", PrincipalId::user("carol"), 99),
573 Revoked::membership("namespace", "band", PrincipalId::user("alice"), 99),
574 ]);
575 assert_eq!(r.admitted(&r.sign(&ed(10))).len(), 3);
576 let mut s = ed(10);
578 s.members.push(member("zed", "guest", &[]));
579 assert!(!r.admitted(&r.sign(&s)).contains(&"zed#guest".to_owned()));
580 }
581
582 #[test]
583 fn a_via_without_its_revocation_key_refuses_the_snapshot() {
584 let r = rig();
585 let mut s = ed(10);
586 s.members.push(member("zed", "guest", &["band"]));
587 match r.verify(&r.sign(&s), IAT) {
588 Err(SnapshotError::MissingRevocationKey { kind, id }) => assert_eq!((kind.as_str(), id.as_str()), ("namespace", "band")),
589 other => panic!("expected MissingRevocationKey, got {other:?}"),
590 }
591 }
592
593 #[test]
594 fn a_snapshot_is_refused_as_every_other_credential() {
595 let r = rig();
596 let token = r.sign(&ed(10));
597 let keys = KeySetVerifier::from_key_set(KeySet::from_doc(r.doc.clone()).unwrap());
599 let as_value = pollster::block_on(keys.verify::<serde_json::Value>(&token, IAT, ISS, None));
600 assert!(matches!(as_value, Err(VerifyError::SignatureMismatch)));
601 let pinned = PasetoV4PublicVerifier::from_public_key(&pub_bytes(&r.issuer)).unwrap();
602 assert!(pinned.verify_mcp_at(&token, IAT, "iss-1").is_err());
603 assert!(pinned.verify_at(&token, IAT).is_err());
604 let standing = StandingVerifier::new(r.edge.trust().clone(), r.replica.clone());
606 assert!(matches!(
607 pollster::block_on(standing.verify_standing_at(&token, &PeerKey::ed25519([1; 32]), IAT)),
608 Err(StandingError::Verify(VerifyError::SignatureMismatch))
609 ));
610 assert!(pollster::block_on(r.replica.adopt(&token)).is_err());
612 assert_eq!(r.replica.epoch(), None);
613 }
614
615 #[test]
616 fn every_other_credential_is_refused_as_a_snapshot() {
617 let r = rig();
618 let unasserted = sign_with(&r.issuer.secret, "iss-1", &ed(10), None);
620 assert!(matches!(
621 r.verify(&unasserted, IAT),
622 Err(SnapshotError::Verify(VerifyError::SignatureMismatch))
623 ));
624 for ia in [StandingBinding::IMPLICIT_ASSERTION, RevocationSet::IMPLICIT_ASSERTION] {
626 let cross = sign_with(&r.issuer.secret, "iss-1", &ed(10), Some(ia));
627 assert!(matches!(r.verify(&cross, IAT), Err(SnapshotError::Verify(VerifyError::SignatureMismatch))));
628 }
629 let binding = StandingBinding {
631 issuer: ISS.into(),
632 sub: UserId::new("alice"),
633 device: DeviceId::new("node:a"),
634 peer_key: PeerKey::ed25519([1; 32]),
635 seq: 5,
636 iat: IAT,
637 jti: "b".into(),
638 lease: Lease::new(IAT, IAT + 60, None).unwrap(),
639 };
640 let binding = sign_with(&r.issuer.secret, "iss-1", &binding, Some(StandingBinding::IMPLICIT_ASSERTION));
641 let set = RevocationSet::new(ISS, 3, vec![]);
642 let set = sign_with(&r.issuer.secret, "iss-1", &set, Some(RevocationSet::IMPLICIT_ASSERTION));
643 let mcp = McpClaims::new(ISS, "https://aud.test", PrincipalId::user("alice"), IAT, IAT + 900, "j", vec![]);
644 let mcp = sign_with(&r.issuer.secret, "iss-1", &mcp, None);
645 let claims = Claims::new(UserId::new("alice"), DeviceId::new("node:a"), DeviceBinding::LanPair, IAT, IAT + 900);
646 let session = sign_with(&r.issuer.secret, "iss-1", &serde_json::json!({ "cheers": claims }), None);
647 for token in [binding, set, mcp, session] {
648 assert!(matches!(r.verify(&token, IAT), Err(SnapshotError::Verify(_))));
649 }
650 let by_assertion = sign_with(&r.assertion.secret, "asr-1", &ed(10), Some(SetSnapshot::IMPLICIT_ASSERTION));
652 assert!(matches!(
653 r.verify(&by_assertion, IAT),
654 Err(SnapshotError::Verify(VerifyError::KeyRoleRejected { .. }))
655 ));
656 let mut foreign = ed(10);
657 foreign.issuer = "https://elsewhere.test".into();
658 assert!(matches!(
659 r.verify(&r.sign(&foreign), IAT),
660 Err(SnapshotError::Verify(VerifyError::BadIssuer { .. }))
661 ));
662 assert_eq!(r.edge.ledger().newest("namespace", "ed"), None, "nothing refused reached the ledger");
663 }
664
665 #[test]
666 fn ledger_export_import_round_trips_and_refuses_a_foreign_issuer() {
667 let ledger = SnapshotLedger::new(ISS);
668 ledger.observe("namespace", "ed", 9);
669 ledger.observe("namespace", "band", 4);
670 ledger.observe("team", "t", 2);
671 let json = serde_json::to_string(&ledger.export()).unwrap();
672 let back: SnapshotLedgerDoc = serde_json::from_str(&json).unwrap();
673 assert_eq!(back, ledger.export());
674 let restored = SnapshotLedger::new(ISS);
675 restored.import(&back).unwrap();
676 assert_eq!(restored.export(), ledger.export());
677
678 let other = SnapshotLedger::new(ISS);
679 other.observe("namespace", "ed", 12);
680 other.observe("namespace", "band", 1);
681 restored.import(&other.export()).unwrap();
682 assert_eq!(restored.newest("namespace", "ed"), Some(12));
683 assert_eq!(restored.newest("namespace", "band"), Some(4));
684 assert!(restored.import(&SnapshotLedger::new("https://elsewhere.test").export()).is_err());
685 }
686}