use std::pin::Pin;
use crate::{
crypto::{
KeyMatchStrength,
refreshable::ScheduledRefreshable,
verifier::{JwsVerifier, KeyMatch, VerifyError},
},
error::Error,
platform::{Duration, MaybeSendBoxFuture, MaybeSendFuture, MaybeSendSync},
};
pub struct ScheduledRefreshVerifier<V> {
inner: ScheduledRefreshable<V>,
}
impl<V: std::fmt::Debug> std::fmt::Debug for ScheduledRefreshVerifier<V> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ScheduledRefreshVerifier")
.field("inner", &self.inner)
.finish_non_exhaustive()
}
}
#[bon::bon]
impl<V: JwsVerifier + 'static> ScheduledRefreshVerifier<V> {
#[builder]
pub async fn new(
factory: impl Fn() -> Pin<Box<dyn MaybeSendFuture<Output = Result<V, 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 })
}
}
impl<V: JwsVerifier + 'static> JwsVerifier for ScheduledRefreshVerifier<V> {
fn key_match(&self, key_match: &KeyMatch<'_>) -> Option<KeyMatchStrength> {
self.inner.load().key_match(key_match)
}
fn verify<'a>(
&'a self,
input: &'a [u8],
signature: &'a [u8],
key_match: &'a KeyMatch<'a>,
) -> MaybeSendBoxFuture<'a, Result<(), VerifyError>> {
Box::pin(async move {
self.inner.poll_refresh_ahead().await;
self.inner
.load_full()
.verify(input, signature, key_match)
.await
})
}
fn try_refresh(&self) -> MaybeSendBoxFuture<'_, bool> {
Box::pin(self.inner.try_refresh_on_miss())
}
}
#[cfg(test)]
mod tests {
use std::sync::{
Arc,
atomic::{AtomicUsize, Ordering},
};
use super::*;
#[derive(Debug)]
struct FlagVerifier {
accept: bool,
}
impl JwsVerifier for FlagVerifier {
fn key_match(&self, _key_match: &KeyMatch<'_>) -> Option<KeyMatchStrength> {
self.accept.then_some(KeyMatchStrength::ByAlgorithm)
}
fn verify<'a>(
&'a self,
_input: &'a [u8],
_signature: &'a [u8],
_key_match: &'a KeyMatch<'a>,
) -> MaybeSendBoxFuture<'a, Result<(), VerifyError>> {
let accept = self.accept;
Box::pin(async move {
if accept {
Ok(())
} else {
Err(VerifyError::SignatureMismatch)
}
})
}
}
#[allow(clippy::type_complexity)]
fn retiring_factory() -> (
Arc<AtomicUsize>,
impl Fn() -> Pin<Box<dyn MaybeSendFuture<Output = Result<FlagVerifier, Error>>>>
+ MaybeSendSync
+ 'static,
) {
let builds = Arc::new(AtomicUsize::new(0));
let counter = builds.clone();
let factory = move || {
let n = counter.fetch_add(1, Ordering::SeqCst);
Box::pin(async move { Ok(FlagVerifier { accept: n == 0 }) })
as Pin<Box<dyn MaybeSendFuture<Output = Result<FlagVerifier, Error>>>>
};
(builds, factory)
}
fn km() -> KeyMatch<'static> {
KeyMatch {
alg: "ES256",
kid: None,
}
}
#[tokio::test]
async fn stale_keyset_reloads_before_verify_and_retires_the_key() {
let (builds, factory) = retiring_factory();
let verifier = ScheduledRefreshVerifier::builder()
.factory(factory)
.ttl(Duration::ZERO)
.min_refresh_interval(Duration::ZERO)
.build()
.await
.unwrap();
verifier
.verify(b"input", b"sig", &km())
.await
.expect_err("the retired key is dropped by the staleness reload");
assert_eq!(
builds.load(Ordering::SeqCst),
2,
"initial build + one reload"
);
}
#[tokio::test]
async fn fresh_keyset_is_not_reloaded() {
let (builds, factory) = retiring_factory();
let verifier = ScheduledRefreshVerifier::builder()
.factory(factory)
.ttl(Duration::from_hours(1))
.build()
.await
.unwrap();
verifier
.verify(b"input", b"sig", &km())
.await
.expect("still on gen-0, which accepts");
assert_eq!(builds.load(Ordering::SeqCst), 1, "no reload while fresh");
}
#[tokio::test]
async fn read_path_reload_is_rate_limited() {
let (builds, factory) = retiring_factory();
let verifier = ScheduledRefreshVerifier::builder()
.factory(factory)
.ttl(Duration::ZERO)
.min_refresh_interval(Duration::from_hours(1))
.build()
.await
.unwrap();
let _ = verifier.verify(b"input", b"sig", &km()).await;
let _ = verifier.verify(b"input", b"sig", &km()).await;
assert_eq!(
builds.load(Ordering::SeqCst),
2,
"initial build + one reload; the second read is rate-limited"
);
}
#[derive(Debug)]
struct MissVerifier {
present: bool,
}
impl JwsVerifier for MissVerifier {
fn key_match(&self, _key_match: &KeyMatch<'_>) -> Option<KeyMatchStrength> {
self.present.then_some(KeyMatchStrength::ByAlgorithm)
}
fn verify<'a>(
&'a self,
_input: &'a [u8],
_signature: &'a [u8],
_key_match: &'a KeyMatch<'a>,
) -> MaybeSendBoxFuture<'a, Result<(), VerifyError>> {
let present = self.present;
Box::pin(async move {
if present {
Ok(())
} else {
Err(VerifyError::NoMatchingKey)
}
})
}
}
#[allow(clippy::type_complexity)]
fn adding_factory() -> (
Arc<AtomicUsize>,
impl Fn() -> Pin<Box<dyn MaybeSendFuture<Output = Result<MissVerifier, Error>>>>
+ MaybeSendSync
+ 'static,
) {
let builds = Arc::new(AtomicUsize::new(0));
let counter = builds.clone();
let factory = move || {
let n = counter.fetch_add(1, Ordering::SeqCst);
Box::pin(async move { Ok(MissVerifier { present: n >= 1 }) })
as Pin<Box<dyn MaybeSendFuture<Output = Result<MissVerifier, Error>>>>
};
(builds, factory)
}
#[tokio::test]
async fn retrying_over_scheduled_picks_up_an_added_key_within_ttl() {
use crate::crypto::verifier::RetryingVerifier;
let (builds, factory) = adding_factory();
let scheduled = ScheduledRefreshVerifier::builder()
.factory(factory)
.ttl(Duration::from_hours(1)) .min_refresh_interval(Duration::ZERO) .build()
.await
.unwrap();
let verifier = RetryingVerifier::new(scheduled);
verifier
.verify(b"input", b"sig", &km())
.await
.expect("the miss reloads within the TTL and the added key verifies");
assert_eq!(
builds.load(Ordering::SeqCst),
2,
"initial build + one miss-driven reload"
);
}
}