use std::{
collections::HashMap,
error::Error,
future::Future,
pin::Pin,
sync::Arc,
time::{Duration, SystemTime, UNIX_EPOCH},
};
use jsonwebtoken::{Algorithm, DecodingKey, Validation, decode, decode_header};
use p256::ecdsa::VerifyingKey;
use serde::Deserialize;
use crate::{
device::{DeviceKey, DeviceKeyError, EcJwk},
profile::Platform,
};
const TOKEN_ALG: &str = "ES256";
pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
pub trait IssuerKeys: Send + Sync {
fn key<'a>(
&'a self,
kid: &'a str,
) -> BoxFuture<'a, Result<Option<VerifyingKey>, KeysUnavailable>>;
}
#[derive(Debug, thiserror::Error)]
#[error("issuer keys are unavailable")]
pub struct KeysUnavailable(#[source] pub Box<dyn Error + Send + Sync>);
#[derive(Debug, Clone, Default)]
pub struct StaticKeys(HashMap<String, VerifyingKey>);
impl StaticKeys {
pub fn from_jwks(document: &[u8]) -> Result<Self, JwksError> {
parse_jwks(document).map(Self)
}
#[must_use]
pub fn with_key(mut self, kid: impl Into<String>, key: VerifyingKey) -> Self {
self.0.insert(kid.into(), key);
self
}
}
impl IssuerKeys for StaticKeys {
fn key<'a>(
&'a self,
kid: &'a str,
) -> BoxFuture<'a, Result<Option<VerifyingKey>, KeysUnavailable>> {
Box::pin(async move { Ok(self.0.get(kid).copied()) })
}
}
#[derive(Debug, thiserror::Error)]
#[error("not a JWKS document")]
pub struct JwksError(#[source] serde_json::Error);
pub fn parse_jwks(document: &[u8]) -> Result<HashMap<String, VerifyingKey>, JwksError> {
#[derive(Deserialize)]
struct JwkSet {
keys: Vec<Jwk>,
}
#[derive(Deserialize)]
struct Jwk {
kid: Option<String>,
alg: Option<String>,
#[serde(rename = "use")]
key_use: Option<String>,
#[serde(flatten)]
ec: Option<EcJwk>,
}
let set: JwkSet = serde_json::from_slice(document).map_err(JwksError)?;
Ok(set
.keys
.into_iter()
.filter(|jwk| jwk.alg.as_deref().is_none_or(|alg| alg == TOKEN_ALG))
.filter(|jwk| {
jwk.key_use
.as_deref()
.is_none_or(|key_use| key_use == "sig")
})
.filter_map(|jwk| Some((jwk.kid?, jwk.ec?.to_verifying_key().ok()?)))
.collect())
}
#[derive(Debug, thiserror::Error)]
pub enum TokenError {
#[error("integrity token is malformed")]
Malformed,
#[error("integrity token is not ES256")]
UnsupportedAlgorithm,
#[error("integrity token issuer is not trusted")]
UntrustedIssuer,
#[error("integrity token kid is unknown")]
UnknownKey,
#[error(transparent)]
KeysUnavailable(#[from] KeysUnavailable),
#[error("integrity token signature does not verify")]
BadSignature,
#[error("integrity token is expired")]
Expired,
#[error("integrity token is not valid yet")]
NotYetValid,
#[error("integrity token audience is not accepted")]
WrongAudience,
#[error("integrity token has no pass claim")]
MissingPass,
#[error("device failed the attestation check")]
IntegrityFailed,
#[error("integrity token platform is not supported")]
UnsupportedPlatform,
#[error(transparent)]
DeviceKey(#[from] DeviceKeyError),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AttestedDevice {
pub key: DeviceKey,
pub platform: Platform,
pub issuer: String,
pub audience: String,
pub expires_at: SystemTime,
}
#[derive(Clone)]
pub struct TokenVerifier {
issuers: HashMap<String, Arc<dyn IssuerKeys>>,
audiences: Vec<String>,
validation: Validation,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum TokenVerifierConfigError {
#[error("at least one trusted issuer is required")]
NoIssuer,
#[error("at least one audience is required")]
NoAudience,
}
#[derive(Deserialize)]
#[serde(untagged)]
enum Audience {
One(String),
Many(Vec<String>),
}
#[derive(Deserialize)]
struct Claims {
#[serde(rename = "iss")]
issuer: String,
#[serde(rename = "aud")]
audience: Audience,
#[serde(rename = "exp")]
expires_at: u64,
#[serde(rename = "nbf")]
not_before: Option<u64>,
#[serde(rename = "pass")]
attestation_passed: Option<bool>,
platform: String,
#[serde(rename = "cnf")]
confirmation: Confirmation,
}
#[derive(Deserialize)]
struct Confirmation {
jwk: EcJwk,
}
impl TokenVerifier {
pub fn new(
issuers: impl IntoIterator<Item = (String, Arc<dyn IssuerKeys>)>,
audiences: impl IntoIterator<Item = String>,
) -> Result<Self, TokenVerifierConfigError> {
let issuers: HashMap<_, _> = issuers.into_iter().collect();
let audiences: Vec<_> = audiences
.into_iter()
.filter(|audience| !audience.trim().is_empty())
.collect();
if issuers.is_empty() {
return Err(TokenVerifierConfigError::NoIssuer);
}
if audiences.is_empty() {
return Err(TokenVerifierConfigError::NoAudience);
}
let mut validation = Validation::new(Algorithm::ES256);
validation.set_audience(&audiences);
validation.validate_exp = false;
validation.validate_nbf = false;
Ok(Self {
issuers,
audiences,
validation,
})
}
pub async fn verify(&self, token: &str, now: SystemTime) -> Result<AttestedDevice, TokenError> {
let header = decode_header(token).map_err(token_error)?;
if header.alg != Algorithm::ES256 {
return Err(TokenError::UnsupportedAlgorithm);
}
if header.crit.is_some() {
return Err(TokenError::Malformed);
}
let kid = header
.kid
.filter(|kid| !kid.is_empty())
.ok_or(TokenError::Malformed)?;
let unverified: Claims =
jsonwebtoken::dangerous::insecure_decode_claims(token).map_err(token_error)?;
let keys = self
.issuers
.get(&unverified.issuer)
.ok_or(TokenError::UntrustedIssuer)?;
let issuer_key = keys.key(&kid).await?.ok_or(TokenError::UnknownKey)?;
let key = DecodingKey::from_ec_der(issuer_key.to_encoded_point(false).as_bytes());
let claims = decode::<Claims>(token, &key, &self.validation)
.map_err(token_error)?
.claims;
let now_secs = now
.duration_since(UNIX_EPOCH)
.map_or(0, |elapsed| elapsed.as_secs());
if now_secs >= claims.expires_at {
return Err(TokenError::Expired);
}
if claims.not_before.is_some_and(|start| now_secs < start) {
return Err(TokenError::NotYetValid);
}
let audience = self
.accepted_audience(&claims.audience)
.ok_or(TokenError::WrongAudience)?;
match claims.attestation_passed {
None => return Err(TokenError::MissingPass),
Some(false) => return Err(TokenError::IntegrityFailed),
Some(true) => {}
}
let platform =
Platform::from_claim(&claims.platform).ok_or(TokenError::UnsupportedPlatform)?;
let key = DeviceKey::new(claims.confirmation.jwk.to_verifying_key()?);
Ok(AttestedDevice {
key,
platform,
issuer: claims.issuer,
audience: audience.to_owned(),
expires_at: UNIX_EPOCH
.checked_add(Duration::from_secs(claims.expires_at))
.ok_or(TokenError::Malformed)?,
})
}
fn accepted_audience(&self, audience: &Audience) -> Option<&str> {
let offered: &[String] = match audience {
Audience::One(audience) => std::slice::from_ref(audience),
Audience::Many(audiences) => audiences,
};
self.audiences
.iter()
.find(|accepted| offered.contains(accepted))
.map(String::as_str)
}
}
fn token_error(error: jsonwebtoken::errors::Error) -> TokenError {
use jsonwebtoken::errors::ErrorKind;
match error.into_kind() {
ErrorKind::InvalidSignature => TokenError::BadSignature,
ErrorKind::InvalidAlgorithm
| ErrorKind::InvalidAlgorithmName
| ErrorKind::UnsupportedAlgorithm => TokenError::UnsupportedAlgorithm,
ErrorKind::InvalidIssuer => TokenError::UntrustedIssuer,
ErrorKind::InvalidAudience => TokenError::WrongAudience,
_ => TokenError::Malformed,
}
}