use std::{pin::Pin, sync::Arc};
use crate::{
crypto::{
refreshable::ScheduledRefreshable,
signer::{JwsSigner, JwsSignerSelector},
},
error::Error,
platform::{Duration, MaybeSendBoxFuture, MaybeSendFuture, MaybeSendSync},
};
#[derive(Debug)]
pub struct ScheduledRefreshSigner<S> {
inner: Arc<ScheduledRefreshable<S>>,
}
impl<S> Clone for ScheduledRefreshSigner<S> {
fn clone(&self) -> Self {
Self {
inner: Arc::clone(&self.inner),
}
}
}
#[bon::bon]
impl<S: std::fmt::Debug + MaybeSendSync + 'static> ScheduledRefreshSigner<S> {
#[builder]
pub async fn new(
factory: impl Fn() -> Pin<Box<dyn MaybeSendFuture<Output = Result<S, Error>>>>
+ MaybeSendSync
+ 'static,
#[builder(default = Duration::from_hours(1))]
ttl: Duration,
#[builder(default = Duration::from_secs(30))]
failure_backoff: Duration,
#[builder(default = Duration::from_mins(1))]
min_refresh_interval: Duration,
) -> Result<Self, Error> {
let inner = ScheduledRefreshable::builder()
.factory(factory)
.ttl(ttl)
.failure_backoff(failure_backoff)
.min_refresh_interval(min_refresh_interval)
.build()
.await?;
Ok(Self {
inner: Arc::new(inner),
})
}
pub async fn refresh_if_stale(&self) -> bool {
self.inner.refresh_if_stale().await
}
pub async fn refresh(&self) -> Result<bool, Error> {
self.inner.refresh().await
}
}
impl<S> JwsSignerSelector for ScheduledRefreshSigner<S>
where
S: JwsSignerSelector + 'static,
{
fn select_signer(&self) -> MaybeSendBoxFuture<'_, Arc<dyn JwsSigner>> {
Box::pin(async move {
self.inner.poll_refresh_ahead().await;
let selector = self.inner.load_full();
selector.select_signer().await
})
}
}
impl<S> super::AsymmetricJwsSignerSelector for ScheduledRefreshSigner<S>
where
S: super::AsymmetricJwsSignerSelector + 'static,
{
fn select_asymmetric_signer(
&self,
) -> MaybeSendBoxFuture<'_, Arc<dyn super::AsymmetricJwsSigner>> {
Box::pin(async move {
self.inner.poll_refresh_ahead().await;
let selector = self.inner.load_full();
selector.select_asymmetric_signer().await
})
}
fn select_signer_by_thumbprint<'a>(
&'a self,
thumbprint: &'a str,
) -> MaybeSendBoxFuture<'a, Option<Arc<dyn super::AsymmetricJwsSigner>>> {
Box::pin(async move {
self.inner.poll_refresh_ahead().await;
let selector = self.inner.load_full();
selector.select_signer_by_thumbprint(thumbprint).await
})
}
}
#[cfg(test)]
mod tests {
use std::{
borrow::Cow,
sync::atomic::{AtomicUsize, Ordering},
};
use rstest::rstest;
use super::*;
use crate::{crypto::signer::JwsSigner, platform::MaybeSendBoxFuture};
#[derive(Debug)]
struct TaggedSigner {
kid: String,
}
impl JwsSigner for TaggedSigner {
fn jws_algorithm(&self) -> Cow<'_, str> {
"ES256".into()
}
fn key_id(&self) -> Option<Cow<'_, str>> {
Some(Cow::Borrowed(&self.kid))
}
fn sign<'a>(&'a self, _input: &'a [u8]) -> MaybeSendBoxFuture<'a, Result<Vec<u8>, Error>> {
Box::pin(async { Ok(vec![0x01]) })
}
}
#[derive(Debug)]
struct GenSelector {
kid: String,
}
impl JwsSignerSelector for GenSelector {
fn select_signer(&self) -> MaybeSendBoxFuture<'_, Arc<dyn JwsSigner>> {
let signer: Arc<dyn JwsSigner> = Arc::new(TaggedSigner {
kid: self.kid.clone(),
});
Box::pin(async move { signer })
}
}
async fn generational_signer(
ttl: Duration,
min_refresh_interval: Duration,
) -> ScheduledRefreshSigner<GenSelector> {
let counter = Arc::new(AtomicUsize::new(0));
ScheduledRefreshSigner::builder()
.factory(move || {
let n = counter.fetch_add(1, Ordering::SeqCst);
Box::pin(async move {
Ok(GenSelector {
kid: format!("gen-{n}"),
})
})
})
.ttl(ttl)
.min_refresh_interval(min_refresh_interval)
.build()
.await
.unwrap()
}
async fn current_kid(signer: &ScheduledRefreshSigner<GenSelector>) -> Option<String> {
signer
.inner
.load_full()
.select_signer()
.await
.key_id()
.map(std::borrow::Cow::into_owned)
}
#[tokio::test]
async fn forced_refresh_bypasses_policy() {
let signer = generational_signer(Duration::from_hours(1), Duration::from_mins(1)).await;
assert_eq!(current_kid(&signer).await.as_deref(), Some("gen-0"));
assert!(signer.refresh().await.unwrap());
assert_eq!(current_kid(&signer).await.as_deref(), Some("gen-1"));
}
#[rstest]
#[case::blocked_while_fresh(Duration::from_hours(1), false, "gen-0")]
#[case::allowed_when_stale(Duration::from_secs(0), true, "gen-1")]
#[tokio::test]
async fn refresh_if_stale_respects_ttl_policy(
#[case] ttl: Duration,
#[case] expected_refreshed: bool,
#[case] expected_kid: &str,
) {
let signer = generational_signer(ttl, Duration::from_secs(0)).await;
assert_eq!(signer.refresh_if_stale().await, expected_refreshed);
assert_eq!(current_kid(&signer).await.as_deref(), Some(expected_kid));
}
}