use std::{
sync::Arc,
time::{Duration, SystemTime, UNIX_EPOCH},
};
use http::{HeaderMap, request::Parts, uri::Authority};
use crate::{
base::CanonicalRequest,
profile::{Component, INTEGRITY_TOKEN_HEADER, SIGNATURE_HEADER, SIGNATURE_INPUT_HEADER},
reject::{RejectReason, Rejection},
replay::ReplayGuard,
signature::{SignatureInputError, SignatureParams, parse_signature, validate_nonce},
token::{AttestedDevice, TokenError, TokenVerifier},
};
pub const DEFAULT_MAX_AGE: Duration = Duration::from_mins(5);
pub const DEFAULT_MAX_FUTURE_SKEW: Duration = Duration::from_secs(60);
pub trait Clock: Send + Sync {
fn now(&self) -> SystemTime;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct SystemClock;
impl Clock for SystemClock {
fn now(&self) -> SystemTime {
SystemTime::now()
}
}
#[derive(Debug, Clone, Copy)]
pub struct FixedClock(pub SystemTime);
impl Clock for FixedClock {
fn now(&self) -> SystemTime {
self.0
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VerifiedAttestedKeyContext {
pub device: AttestedDevice,
pub request_binding: String,
}
#[derive(Clone)]
pub struct Verifier {
tokens: TokenVerifier,
scheme: String,
authority: String,
max_age: Duration,
max_future_skew: Duration,
replay_guard: Option<Arc<dyn ReplayGuard>>,
clock: Arc<dyn Clock>,
}
pub struct VerifierBuilder {
verifier: Verifier,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum VerifierConfigError {
#[error("authority must be set")]
EmptyAuthority,
#[error("authority must be a valid URI authority")]
InvalidAuthority,
#[error("scheme must be set")]
EmptyScheme,
#[error("max age and max future skew must be positive")]
EmptyWindow,
}
impl VerifierBuilder {
#[must_use]
pub fn scheme(mut self, scheme: impl Into<String>) -> Self {
self.verifier.scheme = scheme.into();
self
}
#[must_use]
pub const fn max_age(mut self, max_age: Duration) -> Self {
self.verifier.max_age = max_age;
self
}
#[must_use]
pub const fn max_future_skew(mut self, max_future_skew: Duration) -> Self {
self.verifier.max_future_skew = max_future_skew;
self
}
#[must_use]
pub fn replay_guard(mut self, replay_guard: Arc<dyn ReplayGuard>) -> Self {
self.verifier.replay_guard = Some(replay_guard);
self
}
#[must_use]
pub fn clock(mut self, clock: Arc<dyn Clock>) -> Self {
self.verifier.clock = clock;
self
}
pub fn build(self) -> Result<Verifier, VerifierConfigError> {
let verifier = self.verifier;
if verifier.authority.trim().is_empty() {
return Err(VerifierConfigError::EmptyAuthority);
}
verifier
.authority
.parse::<Authority>()
.map_err(|_| VerifierConfigError::InvalidAuthority)?;
if verifier.scheme.trim().is_empty() {
return Err(VerifierConfigError::EmptyScheme);
}
if verifier.max_age.is_zero() || verifier.max_future_skew.is_zero() {
return Err(VerifierConfigError::EmptyWindow);
}
Ok(verifier)
}
}
impl Verifier {
#[must_use]
pub fn builder(tokens: TokenVerifier, authority: impl Into<String>) -> VerifierBuilder {
VerifierBuilder {
verifier: Self {
tokens,
scheme: "https".to_owned(),
authority: authority.into(),
max_age: DEFAULT_MAX_AGE,
max_future_skew: DEFAULT_MAX_FUTURE_SKEW,
replay_guard: None,
clock: Arc::new(SystemClock),
},
}
}
#[must_use]
pub fn authority(&self) -> &str {
&self.authority
}
pub async fn verify(
&self,
head: &Parts,
body: &[u8],
) -> Result<VerifiedAttestedKeyContext, Rejection> {
let headers = SignedHeaders::extract(&head.headers)?;
let params = SignatureParams::parse(headers.signature_input).map_err(|error| {
let reason = match error {
SignatureInputError::UnsupportedComponent => RejectReason::SignatureBaseIncomplete,
_ => RejectReason::SignatureInputMalformed,
};
Rejection::new(reason).with_source(error)
})?;
let signature = parse_signature(headers.signature)
.map_err(|error| Rejection::new(RejectReason::SignatureMalformed).with_source(error))?;
if let Some(missing) = Component::ALL
.into_iter()
.find(|component| !params.components().contains(component))
{
return Err(Rejection::new(RejectReason::ComponentMissing)
.with_source(format!("{missing} is not covered")));
}
let now = self.clock.now();
self.check_created(params.created(), now)?;
validate_nonce(params.nonce())
.map_err(|error| Rejection::new(RejectReason::NonceInvalid).with_source(error))?;
let device = self
.tokens
.verify(headers.integrity_token, now)
.await
.map_err(token_rejection)?;
let platform = device.platform;
if params.alg() != platform.alg() {
return Err(Rejection::new(RejectReason::AlgMismatch).with_platform(platform));
}
let base = CanonicalRequest::from_http(
&head.method,
&head.uri,
&self.scheme,
&self.authority,
body,
)
.map_err(|error| {
Rejection::new(RejectReason::SignatureBaseIncomplete)
.with_platform(platform)
.with_source(error)
})?
.signature_base(¶ms, headers.integrity_token);
device
.key
.verify(platform, &base.client_data_hash(), &signature)
.map_err(|error| {
Rejection::new(RejectReason::SignatureInvalid)
.with_platform(platform)
.with_source(error)
})?;
let request_binding = base.request_binding();
if let Some(replay_guard) = &self.replay_guard {
let ttl = self.max_age + self.max_future_skew;
let claimed = replay_guard
.claim(&request_binding, ttl)
.await
.map_err(|error| {
Rejection::new(RejectReason::NonceStoreUnavailable)
.with_platform(platform)
.with_source(error)
})?;
if !claimed {
return Err(Rejection::new(RejectReason::Replayed).with_platform(platform));
}
}
Ok(VerifiedAttestedKeyContext {
device,
request_binding,
})
}
fn check_created(&self, created: i64, now: SystemTime) -> Result<(), Rejection> {
let created = UNIX_EPOCH + Duration::from_secs(created.unsigned_abs());
if let Ok(ahead) = created.duration_since(now) {
if ahead > self.max_future_skew {
return Err(Rejection::new(RejectReason::CreatedTooFarInFuture)
.with_source(format!("created is {}s ahead", ahead.as_secs())));
}
} else if let Ok(age) = now.duration_since(created)
&& age > self.max_age
{
return Err(Rejection::new(RejectReason::CreatedTooOld)
.with_source(format!("created is {}s old", age.as_secs())));
}
Ok(())
}
}
fn token_rejection(error: TokenError) -> Rejection {
let reason = match error {
TokenError::KeysUnavailable(_) => RejectReason::JwksUnavailable,
TokenError::IntegrityFailed => RejectReason::DeviceIntegrityFailed,
_ => RejectReason::IntegrityTokenInvalid,
};
Rejection::new(reason).with_source(error)
}
struct SignedHeaders<'a> {
integrity_token: &'a str,
signature_input: &'a str,
signature: &'a str,
}
impl<'a> SignedHeaders<'a> {
fn extract(headers: &'a HeaderMap) -> Result<Self, Rejection> {
let names = [
INTEGRITY_TOKEN_HEADER,
SIGNATURE_INPUT_HEADER,
SIGNATURE_HEADER,
];
if names.iter().any(|name| !headers.contains_key(*name)) {
return Err(RejectReason::HeadersMissing.into());
}
Ok(Self {
integrity_token: single(headers, INTEGRITY_TOKEN_HEADER)
.ok_or(RejectReason::IntegrityTokenInvalid)?,
signature_input: single(headers, SIGNATURE_INPUT_HEADER)
.ok_or(RejectReason::SignatureInputMalformed)?,
signature: single(headers, SIGNATURE_HEADER).ok_or(RejectReason::SignatureMalformed)?,
})
}
}
fn single<'a>(headers: &'a HeaderMap, name: &str) -> Option<&'a str> {
let mut values = headers.get_all(name).iter();
match (values.next(), values.next()) {
(Some(value), None) => value.to_str().ok(),
_ => None,
}
}