use std::collections::HashMap;
use std::sync::{Arc, Mutex, PoisonError};
use async_trait::async_trait;
use cheers_core::{DeviceId, Error, Lease, PeerKey, StandingBinding, StoreError, UserId};
use crate::codec::PasetoV4SecretMinter;
use crate::session::generate_jti;
pub fn next_binding_seq(prev: Option<u64>, now: i64) -> u64 {
prev.unwrap_or(0).saturating_add(1).max(u64::try_from(now).unwrap_or(0))
}
#[async_trait]
pub trait BindingSequenceStore: Send + Sync {
async fn next_binding_seq(&self, device: &DeviceId, now: i64) -> Result<u64, StoreError>;
}
#[async_trait]
impl<T: BindingSequenceStore + ?Sized> BindingSequenceStore for Arc<T> {
async fn next_binding_seq(&self, device: &DeviceId, now: i64) -> Result<u64, StoreError> {
(**self).next_binding_seq(device, now).await
}
}
#[derive(Debug, Clone, Default)]
pub struct MemoryBindingSequenceStore(Arc<Mutex<HashMap<DeviceId, u64>>>);
impl MemoryBindingSequenceStore {
pub fn new() -> Self {
Self::default()
}
pub fn current(&self, device: &DeviceId) -> Option<u64> {
self.0.lock().unwrap_or_else(PoisonError::into_inner).get(device).copied()
}
}
#[async_trait]
impl BindingSequenceStore for MemoryBindingSequenceStore {
async fn next_binding_seq(&self, device: &DeviceId, now: i64) -> Result<u64, StoreError> {
let mut seqs = self.0.lock().unwrap_or_else(PoisonError::into_inner);
let next = next_binding_seq(seqs.get(device).copied(), now);
seqs.insert(device.clone(), next);
Ok(next)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SignedStandingBinding {
pub binding: StandingBinding,
pub token: String,
}
pub struct StandingBinder {
sequences: Arc<dyn BindingSequenceStore>,
minter: PasetoV4SecretMinter,
issuer: String,
kid: String,
refresh_after_seconds: i64,
}
impl std::fmt::Debug for StandingBinder {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("StandingBinder")
.field("issuer", &self.issuer)
.field("kid", &self.kid)
.field("refresh_after_seconds", &self.refresh_after_seconds)
.finish_non_exhaustive()
}
}
impl StandingBinder {
pub const DEFAULT_REFRESH_AFTER_SECONDS: i64 = 7 * 24 * 60 * 60;
pub fn new(
sequences: impl BindingSequenceStore + 'static,
minter: PasetoV4SecretMinter,
issuer: impl Into<String>,
kid: impl Into<String>,
) -> Self {
Self {
sequences: Arc::new(sequences),
minter,
issuer: issuer.into(),
kid: kid.into(),
refresh_after_seconds: Self::DEFAULT_REFRESH_AFTER_SECONDS,
}
}
pub fn with_refresh_after(mut self, seconds: i64) -> Self {
self.refresh_after_seconds = seconds;
self
}
pub fn issuer(&self) -> &str {
&self.issuer
}
pub async fn revocation_seq(&self, device: &DeviceId, now: i64) -> Result<u64, Error> {
Ok(self.sequences.next_binding_seq(device, now).await?)
}
pub async fn mint(&self, sub: UserId, device: DeviceId, peer_key: PeerKey, now: i64) -> Result<SignedStandingBinding, Error> {
let seq = self.sequences.next_binding_seq(&device, now).await?;
let binding = StandingBinding {
issuer: self.issuer.clone(),
sub,
device,
peer_key,
seq,
iat: now,
jti: generate_jti(),
lease: Lease::new(now, now.saturating_add(self.refresh_after_seconds), None)?,
};
let token = self.minter.mint_artifact(&binding, &self.kid)?;
Ok(SignedStandingBinding { binding, token })
}
}
#[cfg(test)]
mod tests {
use super::*;
use cheers_verify::{IssuerTrust, ReplicatedRevocations, StandingVerifier};
#[test]
fn next_seq_is_clock_floored_and_strictly_monotonic() {
assert_eq!(next_binding_seq(None, 1_000), 1_000);
assert_eq!(next_binding_seq(None, -5), 1);
assert_eq!(next_binding_seq(Some(1_000), 1_000), 1_001);
assert_eq!(next_binding_seq(Some(5_000), 1_000), 5_001);
assert_eq!(next_binding_seq(Some(10), 9_000), 9_000);
assert_eq!(next_binding_seq(Some(u64::MAX), 0), u64::MAX);
}
#[test]
fn memory_store_advances_per_device() {
pollster::block_on(async {
let store = MemoryBindingSequenceStore::new();
let (a, b) = (DeviceId::new("node:a"), DeviceId::new("node:b"));
assert_eq!(store.next_binding_seq(&a, 100).await.unwrap(), 100);
assert_eq!(store.next_binding_seq(&a, 100).await.unwrap(), 101);
assert_eq!(store.next_binding_seq(&a, 50).await.unwrap(), 102);
assert_eq!(store.next_binding_seq(&b, 50).await.unwrap(), 50);
assert_eq!(store.current(&a), Some(102));
});
}
#[test]
fn minted_binding_verifies_at_the_edge_and_carries_the_sequence() {
pollster::block_on(async {
let (minter, verifier) = PasetoV4SecretMinter::generate().unwrap();
let store = MemoryBindingSequenceStore::new();
let binder = StandingBinder::new(store.clone(), minter, "https://c.test", "k1").with_refresh_after(60);
let key = PeerKey::ed25519([3; 32]);
let first = binder
.mint(UserId::new("alice"), DeviceId::new("node:a"), key.clone(), 1_000)
.await
.unwrap();
assert_eq!(first.binding.seq, 1_000);
assert_eq!(first.binding.lease.refresh_after(), 1_060);
assert!(!first.binding.jti.is_empty());
let trust = IssuerTrust::pinned("https://c.test", verifier);
let edge = StandingVerifier::new(trust.clone(), ReplicatedRevocations::new(trust));
let ok = edge.verify_standing_at(&first.token, &key, 1_000).await.unwrap();
assert_eq!(ok.binding, first.binding);
let second = binder
.mint(UserId::new("alice"), DeviceId::new("node:a"), key.clone(), 1_000)
.await
.unwrap();
assert_eq!(second.binding.seq, 1_001);
assert_ne!(second.binding.jti, first.binding.jti);
});
}
}