1use std::collections::HashMap;
29use std::sync::{Arc, Mutex};
30
31use async_trait::async_trait;
32use base64::Engine as _;
33use base64::engine::general_purpose::URL_SAFE_NO_PAD;
34use serde::{Deserialize, Serialize};
35
36use cheers_core::{
37 CodecError, Principal, PrincipalError, PrincipalId, PrincipalKind, PrincipalStatus, StoreError,
38};
39
40use crate::codec::PasetoV4SecretMinter;
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
52#[non_exhaustive]
53#[serde(rename_all = "snake_case")]
54pub enum SigningKeyStatus {
55 Active,
56 Retiring,
57}
58
59#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
67#[non_exhaustive]
68pub struct SigningKey {
69 pub kid: String,
73 pub principal_id: PrincipalId,
77 #[serde(with = "public_key_serde")]
81 pub public_key: [u8; 32],
82 pub status: SigningKeyStatus,
83 pub created_at: i64,
84 pub retire_at: Option<i64>,
89}
90
91impl SigningKey {
92 pub fn new(
99 kid: impl Into<String>,
100 principal_id: PrincipalId,
101 public_key: [u8; 32],
102 status: SigningKeyStatus,
103 created_at: i64,
104 retire_at: Option<i64>,
105 ) -> Self {
106 Self {
107 kid: kid.into(),
108 principal_id,
109 public_key,
110 status,
111 created_at,
112 retire_at,
113 }
114 }
115}
116
117mod public_key_serde {
121 use super::*;
122 use serde::de::Error as DeError;
123
124 pub fn serialize<S: serde::Serializer>(bytes: &[u8; 32], ser: S) -> Result<S::Ok, S::Error> {
125 ser.serialize_str(&URL_SAFE_NO_PAD.encode(bytes))
126 }
127
128 pub fn deserialize<'de, D: serde::Deserializer<'de>>(de: D) -> Result<[u8; 32], D::Error> {
129 let s = String::deserialize(de)?;
130 let decoded = URL_SAFE_NO_PAD
131 .decode(s.as_bytes())
132 .map_err(|e| D::Error::custom(format!("invalid base64url public_key: {e}")))?;
133 decoded
134 .try_into()
135 .map_err(|v: Vec<u8>| D::Error::custom(format!("expected 32 public_key bytes, got {}", v.len())))
136 }
137}
138
139#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
145#[non_exhaustive]
146pub struct NewServicePrincipal {
147 pub desired_id: String,
148}
149
150impl NewServicePrincipal {
151 pub fn new(desired_id: impl Into<String>) -> Self {
152 Self {
153 desired_id: desired_id.into(),
154 }
155 }
156}
157
158#[derive(Clone)]
167#[non_exhaustive]
168pub struct ProvisionedKey {
169 pub principal: Principal,
170 pub signing_key: SigningKey,
171 pub secret_key: [u8; 64],
172}
173
174impl std::fmt::Debug for ProvisionedKey {
175 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
176 f.debug_struct("ProvisionedKey")
178 .field("principal", &self.principal)
179 .field("signing_key", &self.signing_key)
180 .field("secret_key", &"***OMITTED***")
181 .finish()
182 }
183}
184
185#[derive(Debug, Clone, Copy, PartialEq, Eq)]
194#[non_exhaustive]
195pub struct OverlapPolicy {
196 pub overlap_seconds: i64,
197}
198
199impl OverlapPolicy {
200 pub const DEFAULT_OVERLAP_SECONDS: i64 = 24 * 60 * 60;
202
203 pub fn new(overlap_seconds: i64) -> Self {
204 Self { overlap_seconds }
205 }
206
207 pub fn with_overlap_seconds(mut self, seconds: i64) -> Self {
208 self.overlap_seconds = seconds;
209 self
210 }
211}
212
213impl Default for OverlapPolicy {
214 fn default() -> Self {
215 Self {
216 overlap_seconds: Self::DEFAULT_OVERLAP_SECONDS,
217 }
218 }
219}
220
221#[derive(Debug, thiserror::Error)]
229#[non_exhaustive]
230pub enum ServicePrincipalError {
231 #[error("service principal '{0}' already exists")]
233 AlreadyExists(PrincipalId),
234 #[error("unknown service principal '{0}'")]
236 UnknownPrincipal(PrincipalId),
237 #[error(transparent)]
240 Principal(#[from] PrincipalError),
241 #[error("service-principal authority requires kind=service; got {0}")]
243 WrongPrincipalKind(PrincipalKind),
244 #[error("service principal '{0}' has no active signing key")]
248 NoActiveKey(PrincipalId),
249 #[error(transparent)]
251 Codec(#[from] CodecError),
252 #[error(transparent)]
254 Store(#[from] StoreError),
255}
256
257#[async_trait]
269pub trait ServicePrincipalStore: Send + Sync {
270 async fn insert_principal(&self, principal: &Principal) -> Result<(), StoreError>;
274
275 async fn get_principal(
277 &self,
278 id: &PrincipalId,
279 ) -> Result<Option<Principal>, StoreError>;
280
281 async fn insert_signing_key(&self, key: &SigningKey) -> Result<(), StoreError>;
285
286 async fn list_signing_keys(
289 &self,
290 principal: &PrincipalId,
291 ) -> Result<Vec<SigningKey>, StoreError>;
292
293 async fn list_all_signing_keys(&self) -> Result<Vec<SigningKey>, StoreError>;
297
298 async fn retire_signing_key(
303 &self,
304 kid: &str,
305 retire_at: i64,
306 ) -> Result<(), StoreError>;
307
308 async fn prune_retired_keys(&self, now: i64) -> Result<u64, StoreError>;
315}
316
317pub struct ServicePrincipalAuthority<S> {
330 store: S,
331 policy: OverlapPolicy,
332}
333
334impl<S> std::fmt::Debug for ServicePrincipalAuthority<S> {
335 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
336 f.debug_struct("ServicePrincipalAuthority")
337 .field("policy", &self.policy)
338 .finish_non_exhaustive()
339 }
340}
341
342impl<S> ServicePrincipalAuthority<S>
343where
344 S: ServicePrincipalStore,
345{
346 pub fn new(store: S) -> Self {
347 Self {
348 store,
349 policy: OverlapPolicy::default(),
350 }
351 }
352
353 pub fn with_policy(mut self, policy: OverlapPolicy) -> Self {
354 self.policy = policy;
355 self
356 }
357
358 pub fn policy(&self) -> &OverlapPolicy {
359 &self.policy
360 }
361
362 pub fn store(&self) -> &S {
363 &self.store
364 }
365
366 pub async fn provision(
374 &self,
375 input: NewServicePrincipal,
376 now: i64,
377 ) -> Result<ProvisionedKey, ServicePrincipalError> {
378 let id = PrincipalId::service(input.desired_id);
379 let principal = Principal::try_new(id.clone(), None, PrincipalStatus::Active, now)?;
383 match self.store.insert_principal(&principal).await {
384 Ok(()) => {}
385 Err(StoreError::Conflict) => return Err(ServicePrincipalError::AlreadyExists(id)),
386 Err(other) => return Err(other.into()),
387 }
388 let (signing_key, secret_key) = self.mint_signing_key(&id, now).await?;
389 Ok(ProvisionedKey {
390 principal,
391 signing_key,
392 secret_key,
393 })
394 }
395
396 pub async fn rotate(
405 &self,
406 id: &PrincipalId,
407 now: i64,
408 ) -> Result<ProvisionedKey, ServicePrincipalError> {
409 if id.kind != PrincipalKind::Service {
410 return Err(ServicePrincipalError::WrongPrincipalKind(id.kind));
411 }
412 let principal = self
413 .store
414 .get_principal(id)
415 .await?
416 .ok_or_else(|| ServicePrincipalError::UnknownPrincipal(id.clone()))?;
417
418 let retire_at = now + self.policy.overlap_seconds;
419 let existing = self.store.list_signing_keys(id).await?;
420 let active = existing
421 .iter()
422 .find(|k| matches!(k.status, SigningKeyStatus::Active))
423 .ok_or_else(|| ServicePrincipalError::NoActiveKey(id.clone()))?;
424 self.store
425 .retire_signing_key(&active.kid, retire_at)
426 .await?;
427
428 let (signing_key, secret_key) = self.mint_signing_key(id, now).await?;
429 Ok(ProvisionedKey {
430 principal,
431 signing_key,
432 secret_key,
433 })
434 }
435
436 pub async fn prune_retired_keys(
440 &self,
441 now: i64,
442 ) -> Result<u64, ServicePrincipalError> {
443 Ok(self.store.prune_retired_keys(now).await?)
444 }
445
446 pub async fn published_signing_keys(
452 &self,
453 now: i64,
454 ) -> Result<Vec<SigningKey>, ServicePrincipalError> {
455 let all = self.store.list_all_signing_keys().await?;
456 Ok(all
457 .into_iter()
458 .filter(|k| match k.status {
459 SigningKeyStatus::Active => true,
460 SigningKeyStatus::Retiring => k.retire_at.map(|t| t > now).unwrap_or(false),
461 })
462 .collect())
463 }
464
465 async fn mint_signing_key(
468 &self,
469 principal_id: &PrincipalId,
470 now: i64,
471 ) -> Result<(SigningKey, [u8; 64]), ServicePrincipalError> {
472 let (minter, verifier) = PasetoV4SecretMinter::generate()?;
473 let public_bytes = verifier
474 .public_key()
475 .as_bytes()
476 .try_into()
477 .expect("v4.public key is 32 bytes");
478 let secret_slice = minter.secret_key_bytes();
480 let mut secret_key = [0u8; 64];
481 secret_key.copy_from_slice(secret_slice);
482
483 let signing_key = SigningKey {
484 kid: mint_kid(),
485 principal_id: principal_id.clone(),
486 public_key: public_bytes,
487 status: SigningKeyStatus::Active,
488 created_at: now,
489 retire_at: None,
490 };
491 self.store.insert_signing_key(&signing_key).await?;
492 Ok((signing_key, secret_key))
493 }
494}
495
496fn mint_kid() -> String {
499 let mut bytes = [0u8; 16];
500 getrandom::fill(&mut bytes).expect("OS CSPRNG must be available");
501 URL_SAFE_NO_PAD.encode(bytes)
502}
503
504#[derive(Default, Clone, Debug)]
512pub struct MemoryServicePrincipalStore {
513 inner: Arc<Mutex<MemoryServicePrincipalInner>>,
514}
515
516#[derive(Default, Debug)]
517struct MemoryServicePrincipalInner {
518 principals: HashMap<PrincipalId, Principal>,
519 keys: Vec<SigningKey>,
520}
521
522impl MemoryServicePrincipalStore {
523 pub fn new() -> Self {
524 Self::default()
525 }
526}
527
528#[async_trait]
529impl ServicePrincipalStore for MemoryServicePrincipalStore {
530 async fn insert_principal(&self, principal: &Principal) -> Result<(), StoreError> {
531 let mut g = self.inner.lock().expect("service-principal store mutex poisoned");
532 if g.principals.contains_key(&principal.id) {
533 return Err(StoreError::Conflict);
534 }
535 g.principals.insert(principal.id.clone(), principal.clone());
536 Ok(())
537 }
538
539 async fn get_principal(
540 &self,
541 id: &PrincipalId,
542 ) -> Result<Option<Principal>, StoreError> {
543 let g = self.inner.lock().expect("service-principal store mutex poisoned");
544 Ok(g.principals.get(id).cloned())
545 }
546
547 async fn insert_signing_key(&self, key: &SigningKey) -> Result<(), StoreError> {
548 let mut g = self.inner.lock().expect("service-principal store mutex poisoned");
549 if g.keys.iter().any(|k| k.kid == key.kid) {
550 return Err(StoreError::Conflict);
551 }
552 g.keys.push(key.clone());
553 Ok(())
554 }
555
556 async fn list_signing_keys(
557 &self,
558 principal: &PrincipalId,
559 ) -> Result<Vec<SigningKey>, StoreError> {
560 let g = self.inner.lock().expect("service-principal store mutex poisoned");
561 Ok(g.keys
562 .iter()
563 .filter(|k| &k.principal_id == principal)
564 .cloned()
565 .collect())
566 }
567
568 async fn list_all_signing_keys(&self) -> Result<Vec<SigningKey>, StoreError> {
569 let g = self.inner.lock().expect("service-principal store mutex poisoned");
570 Ok(g.keys.clone())
571 }
572
573 async fn retire_signing_key(
574 &self,
575 kid: &str,
576 retire_at: i64,
577 ) -> Result<(), StoreError> {
578 let mut g = self.inner.lock().expect("service-principal store mutex poisoned");
579 let key = g
580 .keys
581 .iter_mut()
582 .find(|k| k.kid == kid)
583 .ok_or(StoreError::NotFound)?;
584 key.status = SigningKeyStatus::Retiring;
585 key.retire_at = Some(retire_at);
586 Ok(())
587 }
588
589 async fn prune_retired_keys(&self, now: i64) -> Result<u64, StoreError> {
590 let mut g = self.inner.lock().expect("service-principal store mutex poisoned");
591 let before = g.keys.len();
592 g.keys.retain(|k| match k.status {
593 SigningKeyStatus::Active => true,
594 SigningKeyStatus::Retiring => k.retire_at.map(|t| t > now).unwrap_or(true),
595 });
596 Ok((before - g.keys.len()) as u64)
597 }
598}
599
600#[cfg(test)]
601mod tests {
602 use super::*;
603 use cheers_verify::PasetoV4PublicVerifier;
604 use pollster::block_on;
605
606 fn rig() -> ServicePrincipalAuthority<MemoryServicePrincipalStore> {
607 ServicePrincipalAuthority::new(MemoryServicePrincipalStore::new())
608 }
609
610 #[test]
613 fn overlap_policy_default_is_24_hours() {
614 let p = OverlapPolicy::default();
615 assert_eq!(p.overlap_seconds, 24 * 60 * 60);
616 let custom = OverlapPolicy::new(3_600).with_overlap_seconds(7_200);
617 assert_eq!(custom.overlap_seconds, 7_200);
618 }
619
620 #[test]
623 fn provision_allocates_principal_and_returns_secret_once() {
624 let authority = rig();
625 block_on(async {
626 let provisioned = authority
627 .provision(NewServicePrincipal::new("yubaba-1"), 1_000)
628 .await
629 .unwrap();
630
631 assert_eq!(
633 provisioned.principal.id,
634 PrincipalId::service("yubaba-1")
635 );
636 assert_eq!(provisioned.principal.bound_to, None);
637 assert_eq!(provisioned.principal.status, PrincipalStatus::Active);
638 assert_eq!(provisioned.principal.created_at, 1_000);
639
640 assert_eq!(provisioned.signing_key.principal_id, PrincipalId::service("yubaba-1"));
642 assert_eq!(provisioned.signing_key.status, SigningKeyStatus::Active);
643 assert!(provisioned.signing_key.retire_at.is_none());
644 assert_eq!(provisioned.signing_key.created_at, 1_000);
645 assert!(!provisioned.signing_key.kid.is_empty());
646
647 assert_eq!(
650 &provisioned.secret_key[32..],
651 &provisioned.signing_key.public_key,
652 "PASETO V4 secret layout is seed||pubkey",
653 );
654 });
655 }
656
657 #[test]
658 fn provision_persisted_pubkey_verifies_tokens_minted_by_returned_secret() {
659 use cheers_core::{
663 Actor, AuthStrength, McpClaims, Owns, PrincipalId, Scope,
664 };
665
666 let authority = rig();
667 block_on(async {
668 let provisioned = authority
669 .provision(NewServicePrincipal::new("yubaba-2"), 1_000)
670 .await
671 .unwrap();
672
673 let off_cheers_minter =
675 PasetoV4SecretMinter::from_secret_key(&provisioned.secret_key).unwrap();
676
677 let verifier =
680 PasetoV4PublicVerifier::from_public_key(&provisioned.signing_key.public_key)
681 .unwrap();
682
683 let mut owns = Owns::default();
684 owns.service = vec!["svc-prod".into()];
685 let claims = McpClaims::new(
686 "https://cheers.example",
687 "https://kamaji.example",
688 PrincipalId::service("yubaba-2"),
689 1_000,
690 1_600,
691 "jti-rt",
692 vec![Scope::OwnershipWrite],
693 )
694 .with_act(Actor::new(PrincipalId::service("yubaba-2")))
695 .with_owns(owns)
696 .with_auth_strength(AuthStrength::Bootstrap);
697
698 let token = off_cheers_minter
699 .mint_mcp(&claims, &provisioned.signing_key.kid)
700 .unwrap();
701 let back = verifier
702 .verify_mcp_at(&token, 1_100, &provisioned.signing_key.kid)
703 .unwrap();
704 assert_eq!(back, claims);
705 });
706 }
707
708 #[test]
709 fn provision_rejects_duplicate_id() {
710 let authority = rig();
711 block_on(async {
712 authority
713 .provision(NewServicePrincipal::new("dup"), 1_000)
714 .await
715 .unwrap();
716 let err = authority
717 .provision(NewServicePrincipal::new("dup"), 1_001)
718 .await
719 .unwrap_err();
720 match err {
721 ServicePrincipalError::AlreadyExists(id) => {
722 assert_eq!(id, PrincipalId::service("dup"));
723 }
724 other => panic!("expected AlreadyExists, got {other:?}"),
725 }
726 });
727 }
728
729 #[test]
732 fn rotate_retires_old_key_and_returns_fresh_secret() {
733 let authority = rig();
734 block_on(async {
735 let first = authority
736 .provision(NewServicePrincipal::new("yubaba-r"), 1_000)
737 .await
738 .unwrap();
739 let id = first.principal.id.clone();
740
741 let second = authority.rotate(&id, 5_000).await.unwrap();
742
743 assert_ne!(second.signing_key.kid, first.signing_key.kid);
745 assert_ne!(second.signing_key.public_key, first.signing_key.public_key);
746 assert_eq!(second.signing_key.status, SigningKeyStatus::Active);
747 assert!(second.signing_key.retire_at.is_none());
748
749 let stored = authority
751 .store
752 .list_signing_keys(&id)
753 .await
754 .unwrap();
755 assert_eq!(stored.len(), 2);
756 let old = stored.iter().find(|k| k.kid == first.signing_key.kid).unwrap();
757 assert_eq!(old.status, SigningKeyStatus::Retiring);
758 assert_eq!(
759 old.retire_at,
760 Some(5_000 + OverlapPolicy::DEFAULT_OVERLAP_SECONDS)
761 );
762 });
763 }
764
765 #[test]
766 fn rotate_respects_custom_overlap_window() {
767 let authority = rig().with_policy(OverlapPolicy::new(60 * 60)); block_on(async {
769 let first = authority
770 .provision(NewServicePrincipal::new("yubaba-c"), 1_000)
771 .await
772 .unwrap();
773 authority.rotate(&first.principal.id, 2_000).await.unwrap();
774 let keys = authority
775 .store
776 .list_signing_keys(&first.principal.id)
777 .await
778 .unwrap();
779 let old = keys.iter().find(|k| k.kid == first.signing_key.kid).unwrap();
780 assert_eq!(old.retire_at, Some(2_000 + 3_600));
781 });
782 }
783
784 #[test]
785 fn rotate_rejects_unknown_principal() {
786 let authority = rig();
787 block_on(async {
788 let err = authority
789 .rotate(&PrincipalId::service("ghost"), 1_000)
790 .await
791 .unwrap_err();
792 match err {
793 ServicePrincipalError::UnknownPrincipal(id) => {
794 assert_eq!(id, PrincipalId::service("ghost"));
795 }
796 other => panic!("expected UnknownPrincipal, got {other:?}"),
797 }
798 });
799 }
800
801 #[test]
802 fn rotate_rejects_non_service_principal_kind() {
803 let authority = rig();
804 block_on(async {
805 let err = authority
807 .rotate(&PrincipalId::user("alice"), 1_000)
808 .await
809 .unwrap_err();
810 match err {
811 ServicePrincipalError::WrongPrincipalKind(k) => {
812 assert_eq!(k, PrincipalKind::User);
813 }
814 other => panic!("expected WrongPrincipalKind, got {other:?}"),
815 }
816 });
817 }
818
819 #[test]
822 fn published_signing_keys_includes_active_and_retiring_within_window() {
823 let authority = rig().with_policy(OverlapPolicy::new(100));
824 block_on(async {
825 let first = authority
826 .provision(NewServicePrincipal::new("yubaba-p"), 1_000)
827 .await
828 .unwrap();
829 authority.rotate(&first.principal.id, 2_000).await.unwrap();
831
832 let live = authority.published_signing_keys(2_050).await.unwrap();
834 assert_eq!(live.len(), 2, "got {live:#?}");
835
836 let live = authority.published_signing_keys(2_099).await.unwrap();
838 assert_eq!(live.len(), 2);
839
840 let live = authority.published_signing_keys(2_100).await.unwrap();
843 assert_eq!(live.len(), 1);
844 assert_eq!(live[0].status, SigningKeyStatus::Active);
845 });
846 }
847
848 #[test]
849 fn prune_retired_keys_removes_expired_only() {
850 let authority = rig().with_policy(OverlapPolicy::new(100));
851 block_on(async {
852 let first = authority
853 .provision(NewServicePrincipal::new("yubaba-x"), 1_000)
854 .await
855 .unwrap();
856 authority.rotate(&first.principal.id, 2_000).await.unwrap();
857
858 let dropped = authority.prune_retired_keys(2_099).await.unwrap();
860 assert_eq!(dropped, 0);
861 assert_eq!(
862 authority
863 .store
864 .list_signing_keys(&first.principal.id)
865 .await
866 .unwrap()
867 .len(),
868 2
869 );
870
871 let dropped = authority.prune_retired_keys(2_100).await.unwrap();
873 assert_eq!(dropped, 1);
874 let remaining = authority
875 .store
876 .list_signing_keys(&first.principal.id)
877 .await
878 .unwrap();
879 assert_eq!(remaining.len(), 1);
880 assert_eq!(remaining[0].status, SigningKeyStatus::Active);
881
882 let dropped = authority.prune_retired_keys(2_200).await.unwrap();
884 assert_eq!(dropped, 0);
885 });
886 }
887
888 #[test]
889 fn published_signing_keys_omits_keys_with_no_retire_at_in_retiring_state() {
890 let store = MemoryServicePrincipalStore::new();
895 let key = SigningKey {
896 kid: "k".into(),
897 principal_id: PrincipalId::service("zombie"),
898 public_key: [0u8; 32],
899 status: SigningKeyStatus::Retiring,
900 created_at: 100,
901 retire_at: None,
902 };
903 block_on(async {
904 store.insert_signing_key(&key).await.unwrap();
905 let authority = ServicePrincipalAuthority::new(store);
906 let live = authority.published_signing_keys(1_000).await.unwrap();
907 assert!(live.is_empty());
908 });
909 }
910
911 #[test]
914 fn signing_key_roundtrips_through_json_with_base64_pubkey() {
915 let key = SigningKey {
916 kid: "kid-abc".into(),
917 principal_id: PrincipalId::service("yubaba"),
918 public_key: [1u8; 32],
919 status: SigningKeyStatus::Active,
920 created_at: 100,
921 retire_at: None,
922 };
923 let json = serde_json::to_string(&key).unwrap();
924 assert!(json.contains("\"public_key\":\""), "got {json}");
926 assert!(!json.contains("[1,1,1"), "must NOT be an array: {json}");
927 let back: SigningKey = serde_json::from_str(&json).unwrap();
928 assert_eq!(back, key);
929 }
930
931 #[test]
932 fn signing_key_deserialize_rejects_wrong_length_pubkey() {
933 let json = r#"{
936 "kid": "k",
937 "principal_id": "svc:x",
938 "public_key": "AAAA",
939 "status": "active",
940 "created_at": 0,
941 "retire_at": null
942 }"#;
943 let err = serde_json::from_str::<SigningKey>(json).unwrap_err();
944 assert!(
945 err.to_string().contains("32 public_key bytes"),
946 "got {err}"
947 );
948 }
949
950 #[test]
953 fn service_principal_store_is_dyn_compatible() {
954 fn _f(_: &dyn ServicePrincipalStore) {}
955 }
956}