1use std::collections::HashMap;
30use std::sync::{Arc, Mutex};
31
32use async_trait::async_trait;
33use base64::Engine as _;
34use base64::engine::general_purpose::URL_SAFE_NO_PAD;
35use serde::{Deserialize, Serialize};
36
37use cheers_core::{
38 DelegationError, Principal, PrincipalError, PrincipalId, PrincipalKind, PrincipalStatus,
39 StoreError, UserDelegation,
40};
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
51#[non_exhaustive]
52#[serde(rename_all = "snake_case")]
53pub enum UserSigningKeyStatus {
54 Active,
55 Revoked,
56}
57
58#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
69#[non_exhaustive]
70pub struct UserSigningKey {
71 pub kid: String,
72 pub user: PrincipalId,
74 #[serde(with = "ed25519_public_key_serde")]
75 pub public_key: [u8; 32],
76 pub status: UserSigningKeyStatus,
77 pub created_at: i64,
78}
79
80impl UserSigningKey {
81 pub fn new(
85 kid: impl Into<String>,
86 user: PrincipalId,
87 public_key: [u8; 32],
88 status: UserSigningKeyStatus,
89 created_at: i64,
90 ) -> Self {
91 Self {
92 kid: kid.into(),
93 user,
94 public_key,
95 status,
96 created_at,
97 }
98 }
99}
100
101mod ed25519_public_key_serde {
102 use super::*;
103 use serde::de::Error as DeError;
104
105 pub fn serialize<S: serde::Serializer>(bytes: &[u8; 32], ser: S) -> Result<S::Ok, S::Error> {
106 ser.serialize_str(&URL_SAFE_NO_PAD.encode(bytes))
107 }
108
109 pub fn deserialize<'de, D: serde::Deserializer<'de>>(de: D) -> Result<[u8; 32], D::Error> {
110 let s = String::deserialize(de)?;
111 let raw = URL_SAFE_NO_PAD
112 .decode(s.as_bytes())
113 .map_err(|e| D::Error::custom(format!("invalid base64url public_key: {e}")))?;
114 raw.try_into().map_err(|v: Vec<u8>| {
115 D::Error::custom(format!("expected 32 public_key bytes, got {}", v.len()))
116 })
117 }
118}
119
120#[async_trait]
126pub trait UserSigningKeyStore: Send + Sync {
127 async fn list_active_for_user(
129 &self,
130 user: &PrincipalId,
131 ) -> Result<Vec<UserSigningKey>, StoreError>;
132}
133
134#[derive(Default, Clone, Debug)]
137pub struct MemoryUserSigningKeyStore {
138 inner: Arc<Mutex<Vec<UserSigningKey>>>,
139}
140
141impl MemoryUserSigningKeyStore {
142 pub fn new() -> Self {
143 Self::default()
144 }
145
146 pub fn insert(&self, key: UserSigningKey) {
148 self.inner
149 .lock()
150 .expect("user-signing-key store mutex poisoned")
151 .push(key);
152 }
153}
154
155#[async_trait]
156impl UserSigningKeyStore for MemoryUserSigningKeyStore {
157 async fn list_active_for_user(
158 &self,
159 user: &PrincipalId,
160 ) -> Result<Vec<UserSigningKey>, StoreError> {
161 let g = self.inner.lock().expect("user-signing-key store mutex poisoned");
162 Ok(g.iter()
163 .filter(|k| &k.user == user && matches!(k.status, UserSigningKeyStatus::Active))
164 .cloned()
165 .collect())
166 }
167}
168
169#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
181#[non_exhaustive]
182pub struct CampBootstrapCredential {
183 pub token: String,
184 pub camp_id: PrincipalId,
188 pub created_at: i64,
189 pub expires_at: i64,
190 pub revoked: bool,
192}
193
194#[derive(Debug, Clone, PartialEq, Eq)]
199#[non_exhaustive]
200pub struct NewCampPrincipal {
201 pub bound_to: PrincipalId,
202 pub desired_id: String,
203}
204
205impl NewCampPrincipal {
206 pub fn new(bound_to: PrincipalId, desired_id: impl Into<String>) -> Self {
207 Self {
208 bound_to,
209 desired_id: desired_id.into(),
210 }
211 }
212}
213
214#[derive(Debug, Clone, PartialEq, Eq)]
218#[non_exhaustive]
219pub struct ProvisionedCamp {
220 pub principal: Principal,
221 pub credential: CampBootstrapCredential,
222}
223
224#[async_trait]
236pub trait CampPrincipalStore: Send + Sync {
237 async fn insert_principal(&self, principal: &Principal) -> Result<(), StoreError>;
241
242 async fn get_principal(
245 &self,
246 id: &PrincipalId,
247 ) -> Result<Option<Principal>, StoreError>;
248
249 async fn insert_credential(&self, cred: &CampBootstrapCredential) -> Result<(), StoreError>;
254
255 async fn get_credential(
258 &self,
259 token: &str,
260 ) -> Result<Option<CampBootstrapCredential>, StoreError>;
261
262 async fn insert_delegation(&self, delegation: &UserDelegation)
267 -> Result<(), StoreError>;
268
269 async fn revoke_camps_bound_to(
278 &self,
279 user: &PrincipalId,
280 now: i64,
281 ) -> Result<u64, StoreError>;
282}
283
284#[derive(Default, Clone, Debug)]
289pub struct MemoryCampPrincipalStore {
290 inner: Arc<Mutex<MemoryCampInner>>,
291}
292
293#[derive(Default, Debug)]
294struct MemoryCampInner {
295 principals: HashMap<PrincipalId, Principal>,
296 credentials: HashMap<String, CampBootstrapCredential>,
297 delegations: Vec<UserDelegation>,
298}
299
300impl MemoryCampPrincipalStore {
301 pub fn new() -> Self {
302 Self::default()
303 }
304
305 pub fn delegations(&self) -> Vec<UserDelegation> {
309 self.inner
310 .lock()
311 .expect("camp-principal store mutex poisoned")
312 .delegations
313 .clone()
314 }
315}
316
317#[async_trait]
318impl CampPrincipalStore for MemoryCampPrincipalStore {
319 async fn insert_principal(&self, principal: &Principal) -> Result<(), StoreError> {
320 let mut g = self.inner.lock().expect("camp-principal store mutex poisoned");
321 if g.principals.contains_key(&principal.id) {
322 return Err(StoreError::Conflict);
323 }
324 g.principals.insert(principal.id.clone(), principal.clone());
325 Ok(())
326 }
327
328 async fn get_principal(
329 &self,
330 id: &PrincipalId,
331 ) -> Result<Option<Principal>, StoreError> {
332 let g = self.inner.lock().expect("camp-principal store mutex poisoned");
333 Ok(g.principals.get(id).cloned())
334 }
335
336 async fn insert_credential(&self, cred: &CampBootstrapCredential) -> Result<(), StoreError> {
337 let mut g = self.inner.lock().expect("camp-principal store mutex poisoned");
338 if g.credentials.contains_key(&cred.token) {
339 return Err(StoreError::Conflict);
340 }
341 g.credentials.insert(cred.token.clone(), cred.clone());
342 Ok(())
343 }
344
345 async fn get_credential(
346 &self,
347 token: &str,
348 ) -> Result<Option<CampBootstrapCredential>, StoreError> {
349 let g = self.inner.lock().expect("camp-principal store mutex poisoned");
350 Ok(g.credentials.get(token).cloned())
351 }
352
353 async fn insert_delegation(
354 &self,
355 delegation: &UserDelegation,
356 ) -> Result<(), StoreError> {
357 let mut g = self.inner.lock().expect("camp-principal store mutex poisoned");
358 g.delegations.push(delegation.clone());
359 Ok(())
360 }
361
362 async fn revoke_camps_bound_to(
363 &self,
364 user: &PrincipalId,
365 _now: i64,
366 ) -> Result<u64, StoreError> {
367 let mut g = self.inner.lock().expect("camp-principal store mutex poisoned");
368 let mut camp_ids: Vec<PrincipalId> = Vec::new();
369 let mut newly_revoked = 0u64;
370 for p in g.principals.values_mut() {
371 if p.bound_to.as_ref() == Some(user) && p.status == PrincipalStatus::Active {
372 p.status = PrincipalStatus::Revoked;
373 camp_ids.push(p.id.clone());
374 newly_revoked += 1;
375 }
376 }
377 for cred in g.credentials.values_mut() {
378 if camp_ids.contains(&cred.camp_id) {
379 cred.revoked = true;
380 }
381 }
382 Ok(newly_revoked)
383 }
384}
385
386#[derive(Debug, Clone, Copy, PartialEq, Eq)]
397#[non_exhaustive]
398pub struct CampBootstrapPolicy {
399 pub credential_ttl_seconds: i64,
400}
401
402impl CampBootstrapPolicy {
403 pub const DEFAULT_CREDENTIAL_TTL_SECONDS: i64 = 365 * 24 * 60 * 60;
407
408 pub fn new(credential_ttl_seconds: i64) -> Self {
409 Self {
410 credential_ttl_seconds,
411 }
412 }
413
414 pub fn with_credential_ttl(mut self, seconds: i64) -> Self {
415 self.credential_ttl_seconds = seconds;
416 self
417 }
418}
419
420impl Default for CampBootstrapPolicy {
421 fn default() -> Self {
422 Self {
423 credential_ttl_seconds: Self::DEFAULT_CREDENTIAL_TTL_SECONDS,
424 }
425 }
426}
427
428#[derive(Debug, thiserror::Error)]
435#[non_exhaustive]
436pub enum CampAuthorityError {
437 #[error("camp principal '{0}' already exists")]
439 AlreadyExists(PrincipalId),
440 #[error("camp-authority requires bound_to: kind=user; got {0}")]
443 WrongPrincipalKind(PrincipalKind),
444 #[error(transparent)]
450 InvalidDelegation(#[from] DelegationError),
451 #[error("delegation mismatch: {0}")]
456 DelegationMismatch(&'static str),
457 #[error("delegation expired (expires_at <= now)")]
459 DelegationExpired,
460 #[error("untrusted user signing key for {0}")]
464 UntrustedSigningKey(PrincipalId),
465 #[error("bad delegation signature")]
468 BadSignature,
469 #[error(transparent)]
473 Principal(#[from] PrincipalError),
474 #[error(transparent)]
476 Store(#[from] StoreError),
477}
478
479pub struct CampAuthority<S, K> {
485 store: S,
486 signing_keys: K,
487 policy: CampBootstrapPolicy,
488}
489
490impl<S, K> std::fmt::Debug for CampAuthority<S, K> {
491 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
492 f.debug_struct("CampAuthority")
493 .field("policy", &self.policy)
494 .finish_non_exhaustive()
495 }
496}
497
498impl<S, K> CampAuthority<S, K>
499where
500 S: CampPrincipalStore,
501 K: UserSigningKeyStore,
502{
503 pub fn new(store: S, signing_keys: K) -> Self {
504 Self {
505 store,
506 signing_keys,
507 policy: CampBootstrapPolicy::default(),
508 }
509 }
510
511 pub fn with_policy(mut self, policy: CampBootstrapPolicy) -> Self {
512 self.policy = policy;
513 self
514 }
515
516 pub fn policy(&self) -> &CampBootstrapPolicy {
517 &self.policy
518 }
519
520 pub fn store(&self) -> &S {
521 &self.store
522 }
523
524 pub fn signing_keys(&self) -> &K {
525 &self.signing_keys
526 }
527
528 pub async fn provision(
544 &self,
545 input: NewCampPrincipal,
546 delegation: UserDelegation,
547 now: i64,
548 ) -> Result<ProvisionedCamp, CampAuthorityError> {
549 if input.bound_to.kind != PrincipalKind::User {
550 return Err(CampAuthorityError::WrongPrincipalKind(input.bound_to.kind));
551 }
552 if delegation.bound_to != input.bound_to {
553 return Err(CampAuthorityError::DelegationMismatch(
554 "delegation.bound_to does not match provision.bound_to",
555 ));
556 }
557 if delegation.camp_id != input.desired_id {
558 return Err(CampAuthorityError::DelegationMismatch(
559 "delegation.camp_id does not match provision.desired_id",
560 ));
561 }
562 if delegation.is_expired_at(now) {
563 return Err(CampAuthorityError::DelegationExpired);
564 }
565
566 let trusted = self
567 .signing_keys
568 .list_active_for_user(&delegation.bound_to)
569 .await?;
570 if !trusted
571 .iter()
572 .any(|k| k.public_key == delegation.user_signing_key)
573 {
574 return Err(CampAuthorityError::UntrustedSigningKey(
575 delegation.bound_to.clone(),
576 ));
577 }
578
579 verify_delegation_signature(&delegation)?;
580
581 let camp_id = PrincipalId::camp(input.desired_id.clone());
582 let principal = Principal::try_new(
583 camp_id.clone(),
584 Some(input.bound_to.clone()),
585 PrincipalStatus::Active,
586 now,
587 )?;
588 match self.store.insert_principal(&principal).await {
589 Ok(()) => {}
590 Err(StoreError::Conflict) => {
591 return Err(CampAuthorityError::AlreadyExists(camp_id));
592 }
593 Err(other) => return Err(other.into()),
594 }
595 self.store.insert_delegation(&delegation).await?;
596
597 let credential = CampBootstrapCredential {
598 token: mint_credential_token(),
599 camp_id: camp_id.clone(),
600 created_at: now,
601 expires_at: now + self.policy.credential_ttl_seconds,
602 revoked: false,
603 };
604 self.store.insert_credential(&credential).await?;
605
606 Ok(ProvisionedCamp {
607 principal,
608 credential,
609 })
610 }
611
612 pub async fn revoke_user_cascade(
620 &self,
621 user: &PrincipalId,
622 now: i64,
623 ) -> Result<u64, CampAuthorityError> {
624 if user.kind != PrincipalKind::User {
625 return Err(CampAuthorityError::WrongPrincipalKind(user.kind));
626 }
627 Ok(self.store.revoke_camps_bound_to(user, now).await?)
628 }
629}
630
631fn verify_delegation_signature(
638 delegation: &UserDelegation,
639) -> Result<(), CampAuthorityError> {
640 use ed25519_compact::{PublicKey, Signature};
641
642 let pk = PublicKey::from_slice(&delegation.user_signing_key)
643 .map_err(|_| CampAuthorityError::BadSignature)?;
644 let sig = Signature::from_slice(&delegation.signature)
645 .map_err(|_| CampAuthorityError::BadSignature)?;
646 let payload = delegation.signing_payload();
647 pk.verify(&payload, &sig)
648 .map_err(|_| CampAuthorityError::BadSignature)
649}
650
651fn mint_credential_token() -> String {
654 let mut bytes = [0u8; 32];
655 getrandom::fill(&mut bytes).expect("OS CSPRNG must be available");
656 URL_SAFE_NO_PAD.encode(bytes)
657}
658
659#[cfg(test)]
660mod tests {
661 use super::*;
662 use ed25519_compact::{KeyPair, Seed};
663 use pollster::block_on;
664
665 fn keypair_from_seed(seed: u8) -> KeyPair {
666 KeyPair::from_seed(Seed::from_slice(&[seed; 32]).unwrap())
667 }
668
669 fn signed_delegation(
670 kp: &KeyPair,
671 bound_to: PrincipalId,
672 camp_id: &str,
673 issued_at: i64,
674 expires_at: i64,
675 ) -> UserDelegation {
676 let unsigned = UserDelegation::new(
679 bound_to,
680 camp_id,
681 issued_at,
682 expires_at,
683 *kp.pk,
684 [0u8; 64],
685 )
686 .unwrap();
687 let payload = unsigned.signing_payload();
688 let sig = kp.sk.sign(&payload, None);
689 let mut bytes = [0u8; 64];
690 bytes.copy_from_slice(sig.as_ref());
691 UserDelegation::new(
692 unsigned.bound_to,
693 unsigned.camp_id,
694 unsigned.issued_at,
695 unsigned.expires_at,
696 unsigned.user_signing_key,
697 bytes,
698 )
699 .unwrap()
700 }
701
702 fn trust_key(store: &MemoryUserSigningKeyStore, user: &PrincipalId, pubkey: [u8; 32]) {
703 store.insert(UserSigningKey {
704 kid: "test-key".into(),
705 user: user.clone(),
706 public_key: pubkey,
707 status: UserSigningKeyStatus::Active,
708 created_at: 0,
709 });
710 }
711
712 fn rig() -> (
713 CampAuthority<MemoryCampPrincipalStore, MemoryUserSigningKeyStore>,
714 MemoryCampPrincipalStore,
715 MemoryUserSigningKeyStore,
716 ) {
717 let store = MemoryCampPrincipalStore::new();
718 let keys = MemoryUserSigningKeyStore::new();
719 let authority = CampAuthority::new(store.clone(), keys.clone());
720 (authority, store, keys)
721 }
722
723 #[test]
726 fn policy_default_is_one_year() {
727 let p = CampBootstrapPolicy::default();
728 assert_eq!(p.credential_ttl_seconds, 365 * 24 * 60 * 60);
729 let p = CampBootstrapPolicy::new(60).with_credential_ttl(120);
730 assert_eq!(p.credential_ttl_seconds, 120);
731 }
732
733 #[test]
736 fn provision_happy_path_allocates_camp_and_credential() {
737 let (authority, store, keys) = rig();
738 let user = PrincipalId::user("alice");
739 let kp = keypair_from_seed(1);
740 trust_key(&keys, &user, *kp.pk);
741
742 let delegation =
743 signed_delegation(&kp, user.clone(), "camp-xyz", 1_000, 1_600);
744
745 block_on(async {
746 let prov = authority
747 .provision(
748 NewCampPrincipal::new(user.clone(), "camp-xyz"),
749 delegation.clone(),
750 1_100,
751 )
752 .await
753 .unwrap();
754
755 assert_eq!(prov.principal.id, PrincipalId::camp("camp-xyz"));
757 assert_eq!(prov.principal.bound_to.as_ref(), Some(&user));
758 assert_eq!(prov.principal.status, PrincipalStatus::Active);
759 assert_eq!(prov.principal.created_at, 1_100);
760
761 assert_eq!(prov.credential.camp_id, PrincipalId::camp("camp-xyz"));
763 assert!(!prov.credential.token.is_empty());
764 assert_eq!(prov.credential.created_at, 1_100);
765 assert_eq!(
766 prov.credential.expires_at,
767 1_100 + CampBootstrapPolicy::DEFAULT_CREDENTIAL_TTL_SECONDS
768 );
769 assert!(!prov.credential.revoked);
770
771 let stored_principal = store
773 .get_principal(&PrincipalId::camp("camp-xyz"))
774 .await
775 .unwrap()
776 .unwrap();
777 assert_eq!(stored_principal, prov.principal);
778
779 let stored_cred = store
780 .get_credential(&prov.credential.token)
781 .await
782 .unwrap()
783 .unwrap();
784 assert_eq!(stored_cred, prov.credential);
785
786 assert_eq!(store.delegations(), vec![delegation]);
788 });
789 }
790
791 #[test]
792 fn provision_secret_token_decodes_to_32_bytes() {
793 let (authority, _store, keys) = rig();
794 let user = PrincipalId::user("alice");
795 let kp = keypair_from_seed(2);
796 trust_key(&keys, &user, *kp.pk);
797 let delegation =
798 signed_delegation(&kp, user.clone(), "camp-a", 1_000, 1_600);
799
800 block_on(async {
801 let prov = authority
802 .provision(
803 NewCampPrincipal::new(user, "camp-a"),
804 delegation,
805 1_100,
806 )
807 .await
808 .unwrap();
809 let raw = URL_SAFE_NO_PAD
810 .decode(prov.credential.token.as_bytes())
811 .unwrap();
812 assert_eq!(raw.len(), 32, "credential token is a 256-bit secret");
813 });
814 }
815
816 #[test]
817 fn provision_two_camps_for_same_user_succeed_with_distinct_ids() {
818 let (authority, _, keys) = rig();
819 let user = PrincipalId::user("alice");
820 let kp = keypair_from_seed(3);
821 trust_key(&keys, &user, *kp.pk);
822
823 block_on(async {
824 for camp in ["camp-one", "camp-two"] {
825 let d = signed_delegation(&kp, user.clone(), camp, 1_000, 1_600);
826 authority
827 .provision(
828 NewCampPrincipal::new(user.clone(), camp),
829 d,
830 1_100,
831 )
832 .await
833 .unwrap();
834 }
835 });
836 }
837
838 #[test]
841 fn provision_rejects_non_user_bound_to_at_authority_boundary() {
842 let (authority, _, _) = rig();
845 let kp = keypair_from_seed(4);
846 let user = PrincipalId::user("alice");
851 let d = signed_delegation(&kp, user.clone(), "c", 1_000, 1_600);
852
853 block_on(async {
854 let err = authority
855 .provision(
856 NewCampPrincipal::new(PrincipalId::service("svc"), "c"),
857 d,
858 1_100,
859 )
860 .await
861 .unwrap_err();
862 match err {
863 CampAuthorityError::WrongPrincipalKind(k) => {
864 assert_eq!(k, PrincipalKind::Service);
865 }
866 other => panic!("expected WrongPrincipalKind, got {other:?}"),
867 }
868 });
869 }
870
871 #[test]
872 fn provision_rejects_delegation_bound_to_mismatch() {
873 let (authority, _, keys) = rig();
874 let user_a = PrincipalId::user("alice");
875 let user_b = PrincipalId::user("bob");
876 let kp = keypair_from_seed(5);
877 trust_key(&keys, &user_a, *kp.pk);
880 trust_key(&keys, &user_b, *kp.pk);
881 let d = signed_delegation(&kp, user_b.clone(), "camp-x", 1_000, 1_600);
883
884 block_on(async {
885 let err = authority
887 .provision(
888 NewCampPrincipal::new(user_a.clone(), "camp-x"),
889 d,
890 1_100,
891 )
892 .await
893 .unwrap_err();
894 assert!(
895 matches!(err, CampAuthorityError::DelegationMismatch(msg) if msg.contains("bound_to")),
896 "got {err:?}"
897 );
898 });
899 }
900
901 #[test]
902 fn provision_rejects_delegation_camp_id_mismatch() {
903 let (authority, _, keys) = rig();
904 let user = PrincipalId::user("alice");
905 let kp = keypair_from_seed(6);
906 trust_key(&keys, &user, *kp.pk);
907 let d = signed_delegation(&kp, user.clone(), "camp-x", 1_000, 1_600);
909
910 block_on(async {
911 let err = authority
913 .provision(
914 NewCampPrincipal::new(user, "camp-y"),
915 d,
916 1_100,
917 )
918 .await
919 .unwrap_err();
920 assert!(
921 matches!(err, CampAuthorityError::DelegationMismatch(msg) if msg.contains("camp_id")),
922 "got {err:?}"
923 );
924 });
925 }
926
927 #[test]
928 fn provision_rejects_expired_delegation() {
929 let (authority, _, keys) = rig();
930 let user = PrincipalId::user("alice");
931 let kp = keypair_from_seed(7);
932 trust_key(&keys, &user, *kp.pk);
933 let d = signed_delegation(&kp, user.clone(), "c", 1_000, 1_600);
934
935 block_on(async {
936 let err = authority
937 .provision(NewCampPrincipal::new(user, "c"), d, 1_600)
938 .await
939 .unwrap_err();
940 assert!(matches!(err, CampAuthorityError::DelegationExpired), "got {err:?}");
941 });
942 }
943
944 #[test]
945 fn provision_rejects_untrusted_signing_key() {
946 let (authority, _, _keys) = rig();
947 let user = PrincipalId::user("alice");
948 let kp = keypair_from_seed(8);
949 let d = signed_delegation(&kp, user.clone(), "c", 1_000, 1_600);
951
952 block_on(async {
953 let err = authority
954 .provision(NewCampPrincipal::new(user.clone(), "c"), d, 1_100)
955 .await
956 .unwrap_err();
957 match err {
958 CampAuthorityError::UntrustedSigningKey(u) => assert_eq!(u, user),
959 other => panic!("expected UntrustedSigningKey, got {other:?}"),
960 }
961 });
962 }
963
964 #[test]
965 fn provision_rejects_revoked_signing_key() {
966 let (authority, _, keys) = rig();
968 let user = PrincipalId::user("alice");
969 let kp = keypair_from_seed(9);
970 keys.insert(UserSigningKey {
971 kid: "stale".into(),
972 user: user.clone(),
973 public_key: *kp.pk,
974 status: UserSigningKeyStatus::Revoked,
975 created_at: 0,
976 });
977 let d = signed_delegation(&kp, user.clone(), "c", 1_000, 1_600);
978
979 block_on(async {
980 let err = authority
981 .provision(NewCampPrincipal::new(user, "c"), d, 1_100)
982 .await
983 .unwrap_err();
984 assert!(matches!(err, CampAuthorityError::UntrustedSigningKey(_)), "got {err:?}");
985 });
986 }
987
988 #[test]
989 fn provision_rejects_bad_signature() {
990 let (authority, _, keys) = rig();
991 let user = PrincipalId::user("alice");
992 let kp = keypair_from_seed(10);
993 trust_key(&keys, &user, *kp.pk);
994
995 let mut d = signed_delegation(&kp, user.clone(), "c", 1_000, 1_600);
998 d.signature[0] ^= 0x01;
999
1000 block_on(async {
1001 let err = authority
1002 .provision(NewCampPrincipal::new(user, "c"), d, 1_100)
1003 .await
1004 .unwrap_err();
1005 assert!(matches!(err, CampAuthorityError::BadSignature), "got {err:?}");
1006 });
1007 }
1008
1009 #[test]
1010 fn provision_rejects_signature_from_other_keypair_in_trusted_set() {
1011 let (authority, _, keys) = rig();
1016 let user = PrincipalId::user("alice");
1017 let kp_trusted = keypair_from_seed(11);
1018 let kp_other = keypair_from_seed(12);
1019 trust_key(&keys, &user, *kp_trusted.pk);
1020
1021 let payload_d = UserDelegation::new(
1023 user.clone(),
1024 "c",
1025 1_000,
1026 1_600,
1027 *kp_trusted.pk,
1028 [0u8; 64],
1029 )
1030 .unwrap();
1031 let sig = kp_other.sk.sign(&payload_d.signing_payload(), None);
1032 let mut sig_bytes = [0u8; 64];
1033 sig_bytes.copy_from_slice(sig.as_ref());
1034 let forged = UserDelegation::new(
1035 payload_d.bound_to,
1036 payload_d.camp_id,
1037 payload_d.issued_at,
1038 payload_d.expires_at,
1039 payload_d.user_signing_key,
1040 sig_bytes,
1041 )
1042 .unwrap();
1043
1044 block_on(async {
1045 let err = authority
1046 .provision(NewCampPrincipal::new(user, "c"), forged, 1_100)
1047 .await
1048 .unwrap_err();
1049 assert!(matches!(err, CampAuthorityError::BadSignature), "got {err:?}");
1050 });
1051 }
1052
1053 #[test]
1054 fn provision_rejects_duplicate_camp_id() {
1055 let (authority, _, keys) = rig();
1056 let user = PrincipalId::user("alice");
1057 let kp = keypair_from_seed(13);
1058 trust_key(&keys, &user, *kp.pk);
1059
1060 block_on(async {
1061 let d = signed_delegation(&kp, user.clone(), "dup", 1_000, 1_600);
1062 authority
1063 .provision(
1064 NewCampPrincipal::new(user.clone(), "dup"),
1065 d.clone(),
1066 1_100,
1067 )
1068 .await
1069 .unwrap();
1070 let err = authority
1071 .provision(NewCampPrincipal::new(user, "dup"), d, 1_100)
1072 .await
1073 .unwrap_err();
1074 match err {
1075 CampAuthorityError::AlreadyExists(id) => {
1076 assert_eq!(id, PrincipalId::camp("dup"));
1077 }
1078 other => panic!("expected AlreadyExists, got {other:?}"),
1079 }
1080 });
1081 }
1082
1083 #[test]
1086 fn revoke_user_cascade_flips_only_camps_bound_to_that_user() {
1087 let (authority, store, keys) = rig();
1088 let alice = PrincipalId::user("alice");
1089 let bob = PrincipalId::user("bob");
1090 let kp_a = keypair_from_seed(20);
1091 let kp_b = keypair_from_seed(21);
1092 trust_key(&keys, &alice, *kp_a.pk);
1093 trust_key(&keys, &bob, *kp_b.pk);
1094
1095 block_on(async {
1096 for camp in ["a-1", "a-2"] {
1098 let d = signed_delegation(&kp_a, alice.clone(), camp, 1_000, 1_600);
1099 authority
1100 .provision(NewCampPrincipal::new(alice.clone(), camp), d, 1_100)
1101 .await
1102 .unwrap();
1103 }
1104 let d = signed_delegation(&kp_b, bob.clone(), "b-1", 1_000, 1_600);
1105 let b1 = authority
1106 .provision(NewCampPrincipal::new(bob.clone(), "b-1"), d, 1_100)
1107 .await
1108 .unwrap();
1109
1110 let count = authority.revoke_user_cascade(&alice, 2_000).await.unwrap();
1112 assert_eq!(count, 2);
1113
1114 for camp in ["a-1", "a-2"] {
1115 let p = store
1116 .get_principal(&PrincipalId::camp(camp))
1117 .await
1118 .unwrap()
1119 .unwrap();
1120 assert_eq!(p.status, PrincipalStatus::Revoked);
1121 }
1122 let bob_camp = store
1123 .get_principal(&PrincipalId::camp("b-1"))
1124 .await
1125 .unwrap()
1126 .unwrap();
1127 assert_eq!(bob_camp.status, PrincipalStatus::Active);
1128
1129 let bob_cred = store
1131 .get_credential(&b1.credential.token)
1132 .await
1133 .unwrap()
1134 .unwrap();
1135 assert!(!bob_cred.revoked);
1136 });
1137 }
1138
1139 #[test]
1140 fn revoke_user_cascade_is_idempotent() {
1141 let (authority, _, keys) = rig();
1142 let user = PrincipalId::user("alice");
1143 let kp = keypair_from_seed(30);
1144 trust_key(&keys, &user, *kp.pk);
1145
1146 block_on(async {
1147 let d = signed_delegation(&kp, user.clone(), "c", 1_000, 1_600);
1148 authority
1149 .provision(NewCampPrincipal::new(user.clone(), "c"), d, 1_100)
1150 .await
1151 .unwrap();
1152
1153 let first = authority.revoke_user_cascade(&user, 2_000).await.unwrap();
1154 let second = authority.revoke_user_cascade(&user, 2_001).await.unwrap();
1155 assert_eq!(first, 1);
1156 assert_eq!(second, 0, "second sweep is a no-op");
1157 });
1158 }
1159
1160 #[test]
1161 fn revoke_user_cascade_revokes_associated_credentials() {
1162 let (authority, store, keys) = rig();
1163 let user = PrincipalId::user("alice");
1164 let kp = keypair_from_seed(31);
1165 trust_key(&keys, &user, *kp.pk);
1166
1167 block_on(async {
1168 let d = signed_delegation(&kp, user.clone(), "c", 1_000, 1_600);
1169 let prov = authority
1170 .provision(NewCampPrincipal::new(user.clone(), "c"), d, 1_100)
1171 .await
1172 .unwrap();
1173 assert!(!prov.credential.revoked);
1174
1175 authority.revoke_user_cascade(&user, 2_000).await.unwrap();
1176
1177 let cred = store
1178 .get_credential(&prov.credential.token)
1179 .await
1180 .unwrap()
1181 .unwrap();
1182 assert!(cred.revoked, "cascade must mark the credential revoked too");
1183 });
1184 }
1185
1186 #[test]
1187 fn revoke_user_cascade_rejects_non_user_kind() {
1188 let (authority, _, _) = rig();
1189 block_on(async {
1190 let err = authority
1191 .revoke_user_cascade(&PrincipalId::service("yubaba"), 1_000)
1192 .await
1193 .unwrap_err();
1194 match err {
1195 CampAuthorityError::WrongPrincipalKind(k) => {
1196 assert_eq!(k, PrincipalKind::Service);
1197 }
1198 other => panic!("expected WrongPrincipalKind, got {other:?}"),
1199 }
1200 });
1201 }
1202
1203 #[test]
1206 fn traits_are_dyn_compatible() {
1207 fn _s(_: &dyn CampPrincipalStore) {}
1208 fn _k(_: &dyn UserSigningKeyStore) {}
1209 }
1210
1211 #[test]
1214 fn user_signing_key_roundtrips_through_json_with_base64_pubkey() {
1215 let key = UserSigningKey {
1216 kid: "kid-1".into(),
1217 user: PrincipalId::user("alice"),
1218 public_key: [3u8; 32],
1219 status: UserSigningKeyStatus::Active,
1220 created_at: 100,
1221 };
1222 let json = serde_json::to_string(&key).unwrap();
1223 assert!(json.contains("\"public_key\":\""));
1224 assert!(!json.contains("[3,3,3"), "must NOT be a byte array: {json}");
1225 let back: UserSigningKey = serde_json::from_str(&json).unwrap();
1226 assert_eq!(back, key);
1227 }
1228
1229 #[test]
1230 fn camp_bootstrap_credential_roundtrips_json() {
1231 let cred = CampBootstrapCredential {
1232 token: "tok-1".into(),
1233 camp_id: PrincipalId::camp("c-1"),
1234 created_at: 100,
1235 expires_at: 200,
1236 revoked: false,
1237 };
1238 let json = serde_json::to_string(&cred).unwrap();
1239 let back: CampBootstrapCredential = serde_json::from_str(&json).unwrap();
1240 assert_eq!(back, cred);
1241 }
1242}