1use std::collections::HashMap;
15use std::sync::{Arc, Mutex, PoisonError};
16
17use async_trait::async_trait;
18
19use cheers_core::{DeviceId, Error, Lease, PeerKey, StandingBinding, StoreError, UserId};
20
21use crate::codec::PasetoV4SecretMinter;
22use crate::session::generate_jti;
23
24pub fn next_binding_seq(prev: Option<u64>, now: i64) -> u64 {
28 prev.unwrap_or(0).saturating_add(1).max(u64::try_from(now).unwrap_or(0))
29}
30
31#[async_trait]
35pub trait BindingSequenceStore: Send + Sync {
36 async fn next_binding_seq(&self, device: &DeviceId, now: i64) -> Result<u64, StoreError>;
40}
41
42#[async_trait]
43impl<T: BindingSequenceStore + ?Sized> BindingSequenceStore for Arc<T> {
44 async fn next_binding_seq(&self, device: &DeviceId, now: i64) -> Result<u64, StoreError> {
45 (**self).next_binding_seq(device, now).await
46 }
47}
48
49#[derive(Debug, Clone, Default)]
52pub struct MemoryBindingSequenceStore(Arc<Mutex<HashMap<DeviceId, u64>>>);
53
54impl MemoryBindingSequenceStore {
55 pub fn new() -> Self {
56 Self::default()
57 }
58
59 pub fn current(&self, device: &DeviceId) -> Option<u64> {
61 self.0.lock().unwrap_or_else(PoisonError::into_inner).get(device).copied()
62 }
63}
64
65#[async_trait]
66impl BindingSequenceStore for MemoryBindingSequenceStore {
67 async fn next_binding_seq(&self, device: &DeviceId, now: i64) -> Result<u64, StoreError> {
68 let mut seqs = self.0.lock().unwrap_or_else(PoisonError::into_inner);
69 let next = next_binding_seq(seqs.get(device).copied(), now);
70 seqs.insert(device.clone(), next);
71 Ok(next)
72 }
73}
74
75#[derive(Debug, Clone, PartialEq, Eq)]
77pub struct SignedStandingBinding {
78 pub binding: StandingBinding,
79 pub token: String,
82}
83
84pub struct StandingBinder {
91 sequences: Arc<dyn BindingSequenceStore>,
92 minter: PasetoV4SecretMinter,
93 issuer: String,
94 kid: String,
95 refresh_after_seconds: i64,
96}
97
98impl std::fmt::Debug for StandingBinder {
99 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
100 f.debug_struct("StandingBinder")
101 .field("issuer", &self.issuer)
102 .field("kid", &self.kid)
103 .field("refresh_after_seconds", &self.refresh_after_seconds)
104 .finish_non_exhaustive()
105 }
106}
107
108impl StandingBinder {
109 pub const DEFAULT_REFRESH_AFTER_SECONDS: i64 = 7 * 24 * 60 * 60;
113
114 pub fn new(
115 sequences: impl BindingSequenceStore + 'static,
116 minter: PasetoV4SecretMinter,
117 issuer: impl Into<String>,
118 kid: impl Into<String>,
119 ) -> Self {
120 Self {
121 sequences: Arc::new(sequences),
122 minter,
123 issuer: issuer.into(),
124 kid: kid.into(),
125 refresh_after_seconds: Self::DEFAULT_REFRESH_AFTER_SECONDS,
126 }
127 }
128
129 pub fn with_refresh_after(mut self, seconds: i64) -> Self {
130 self.refresh_after_seconds = seconds;
131 self
132 }
133
134 pub fn issuer(&self) -> &str {
135 &self.issuer
136 }
137
138 pub async fn revocation_seq(&self, device: &DeviceId, now: i64) -> Result<u64, Error> {
144 Ok(self.sequences.next_binding_seq(device, now).await?)
145 }
146
147 pub async fn mint(&self, sub: UserId, device: DeviceId, peer_key: PeerKey, now: i64) -> Result<SignedStandingBinding, Error> {
151 let seq = self.sequences.next_binding_seq(&device, now).await?;
152 let binding = StandingBinding {
153 issuer: self.issuer.clone(),
154 sub,
155 device,
156 peer_key,
157 seq,
158 iat: now,
159 jti: generate_jti(),
160 lease: Lease::new(now, now.saturating_add(self.refresh_after_seconds), None)?,
161 };
162 let token = self.minter.mint_artifact(&binding, &self.kid)?;
163 Ok(SignedStandingBinding { binding, token })
164 }
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170 use cheers_verify::{IssuerTrust, ReplicatedRevocations, StandingVerifier};
171
172 #[test]
173 fn next_seq_is_clock_floored_and_strictly_monotonic() {
174 assert_eq!(next_binding_seq(None, 1_000), 1_000);
175 assert_eq!(next_binding_seq(None, -5), 1);
176 assert_eq!(next_binding_seq(Some(1_000), 1_000), 1_001);
177 assert_eq!(next_binding_seq(Some(5_000), 1_000), 5_001);
179 assert_eq!(next_binding_seq(Some(10), 9_000), 9_000);
182 assert_eq!(next_binding_seq(Some(u64::MAX), 0), u64::MAX);
183 }
184
185 #[test]
186 fn memory_store_advances_per_device() {
187 pollster::block_on(async {
188 let store = MemoryBindingSequenceStore::new();
189 let (a, b) = (DeviceId::new("node:a"), DeviceId::new("node:b"));
190 assert_eq!(store.next_binding_seq(&a, 100).await.unwrap(), 100);
191 assert_eq!(store.next_binding_seq(&a, 100).await.unwrap(), 101);
192 assert_eq!(store.next_binding_seq(&a, 50).await.unwrap(), 102);
193 assert_eq!(store.next_binding_seq(&b, 50).await.unwrap(), 50);
194 assert_eq!(store.current(&a), Some(102));
195 });
196 }
197
198 #[test]
199 fn minted_binding_verifies_at_the_edge_and_carries_the_sequence() {
200 pollster::block_on(async {
201 let (minter, verifier) = PasetoV4SecretMinter::generate().unwrap();
202 let store = MemoryBindingSequenceStore::new();
203 let binder = StandingBinder::new(store.clone(), minter, "https://c.test", "k1").with_refresh_after(60);
204 let key = PeerKey::ed25519([3; 32]);
205 let first = binder
206 .mint(UserId::new("alice"), DeviceId::new("node:a"), key.clone(), 1_000)
207 .await
208 .unwrap();
209 assert_eq!(first.binding.seq, 1_000);
210 assert_eq!(first.binding.lease.refresh_after(), 1_060);
211 assert!(!first.binding.jti.is_empty());
212
213 let trust = IssuerTrust::pinned("https://c.test", verifier);
214 let edge = StandingVerifier::new(trust.clone(), ReplicatedRevocations::new(trust));
215 let ok = edge.verify_standing_at(&first.token, &key, 1_000).await.unwrap();
216 assert_eq!(ok.binding, first.binding);
217
218 let second = binder
219 .mint(UserId::new("alice"), DeviceId::new("node:a"), key.clone(), 1_000)
220 .await
221 .unwrap();
222 assert_eq!(second.binding.seq, 1_001);
223 assert_ne!(second.binding.jti, first.binding.jti);
224 });
225 }
226}