use std::time::{SystemTime, UNIX_EPOCH};
use cbor2::Cbor;
use crate::{iana, tag, CoseMap, Error, Value};
const MAX_CLOCK_SKEW_SECS: u64 = 10 * 60;
#[derive(Clone, Debug, Default, PartialEq, Cbor)]
#[cbor(tag = 61)]
pub struct Claims {
#[cbor(key = 1)]
#[serde(rename = "iss", skip_serializing_if = "Option::is_none", default)]
pub issuer: Option<String>,
#[cbor(key = 2)]
#[serde(rename = "sub", skip_serializing_if = "Option::is_none", default)]
pub subject: Option<String>,
#[cbor(key = 3)]
#[serde(rename = "aud", skip_serializing_if = "Option::is_none", default)]
pub audience: Option<String>,
#[cbor(key = 4)]
#[serde(
rename = "exp",
with = "numeric_date",
skip_serializing_if = "Option::is_none",
default
)]
pub expiration: Option<u64>,
#[cbor(key = 5)]
#[serde(
rename = "nbf",
with = "numeric_date",
skip_serializing_if = "Option::is_none",
default
)]
pub not_before: Option<u64>,
#[cbor(key = 6)]
#[serde(
rename = "iat",
with = "numeric_date",
skip_serializing_if = "Option::is_none",
default
)]
pub issued_at: Option<u64>,
#[cbor(key = 7)]
#[serde(
rename = "cti",
with = "serde_bytes",
skip_serializing_if = "Option::is_none",
default
)]
pub cwt_id: Option<Vec<u8>>,
#[serde(flatten, skip_serializing_if = "CoseMap::is_empty", default)]
pub extra: CoseMap,
}
pub mod numeric_date {
use serde::{de, Deserializer, Serializer};
pub(crate) fn from_f64<E: de::Error>(v: f64) -> Result<u64, E> {
if !v.is_finite() || v.fract() != 0.0 {
return Err(E::custom(
"fractional-second NumericDate is not supported, use whole seconds",
));
}
if v < 0.0 {
return Err(E::custom("pre-epoch NumericDate is not supported"));
}
if v >= u64::MAX as f64 {
return Err(E::custom("NumericDate out of range"));
}
Ok(v as u64)
}
pub fn serialize<S: Serializer>(value: &Option<u64>, serializer: S) -> Result<S::Ok, S::Error> {
match value {
Some(v) => serializer.serialize_u64(*v),
None => serializer.serialize_none(),
}
}
pub fn deserialize<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<Option<u64>, D::Error> {
struct DateVisitor;
impl de::Visitor<'_> for DateVisitor {
type Value = u64;
fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("a NumericDate in whole seconds since the UNIX epoch")
}
fn visit_u64<E: de::Error>(self, v: u64) -> Result<u64, E> {
Ok(v)
}
fn visit_i64<E: de::Error>(self, v: i64) -> Result<u64, E> {
u64::try_from(v).map_err(|_| E::custom("pre-epoch NumericDate is not supported"))
}
fn visit_u128<E: de::Error>(self, v: u128) -> Result<u64, E> {
u64::try_from(v).map_err(|_| E::custom("NumericDate out of range"))
}
fn visit_i128<E: de::Error>(self, v: i128) -> Result<u64, E> {
u64::try_from(v).map_err(|_| E::custom("NumericDate out of range or pre-epoch"))
}
fn visit_f64<E: de::Error>(self, v: f64) -> Result<u64, E> {
from_f64(v)
}
}
struct OptionVisitor;
impl<'de> de::Visitor<'de> for OptionVisitor {
type Value = Option<u64>;
fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("an optional NumericDate")
}
fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
Ok(None)
}
fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
Ok(None)
}
fn visit_some<D2: Deserializer<'de>>(
self,
deserializer: D2,
) -> Result<Self::Value, D2::Error> {
deserializer.deserialize_any(DateVisitor).map(Some)
}
}
deserializer.deserialize_option(OptionVisitor)
}
}
impl Claims {
pub fn new() -> Self {
Claims::default()
}
pub fn from_slice(data: &[u8]) -> Result<Self, Error> {
let data = tag::skip_tag(tag::CBOR_SELF_PREFIX, data);
if !data.starts_with(tag::CWT_PREFIX) && tag::starts_with_cbor_tag(data) {
return Err(Error::Custom("unexpected CBOR tag for CWT Claims".into()));
}
Ok(cbor2::from_slice(data)?)
}
pub fn to_vec(&self) -> Result<Vec<u8>, Error> {
Ok(cbor2::to_canonical_vec(self)?)
}
pub fn to_untagged_vec(&self) -> Result<Vec<u8>, Error> {
let tagged = self.to_vec()?;
Ok(tag::skip_tag(tag::CWT_PREFIX, &tagged).to_vec())
}
}
pub type ClaimsMap = CoseMap;
#[derive(Clone, Debug, Default)]
pub struct ValidatorOptions {
pub expected_issuer: Option<String>,
pub expected_audience: Option<String>,
pub allow_missing_expiration: bool,
pub expect_issued_in_the_past: bool,
pub clock_skew_secs: u64,
pub fixed_now: Option<i64>,
}
#[derive(Clone, Debug)]
pub struct Validator {
opts: ValidatorOptions,
}
fn to_secs(value: u64) -> i64 {
i64::try_from(value).unwrap_or(i64::MAX)
}
fn numeric_date_claim(claims: &ClaimsMap, key: i64) -> Result<Option<i64>, Error> {
match claims.get(key) {
Some(Value::Float(f)) => numeric_date::from_f64::<serde::de::value::Error>(*f)
.map(|secs| Some(to_secs(secs)))
.map_err(|err| Error::UnexpectedType(err.to_string())),
_ => match claims.get_i64(key)? {
Some(secs) if secs < 0 => Err(Error::UnexpectedType(
"pre-epoch NumericDate is not supported".into(),
)),
other => Ok(other),
},
}
}
impl Validator {
pub fn new(opts: ValidatorOptions) -> Result<Self, Error> {
if opts.clock_skew_secs > MAX_CLOCK_SKEW_SECS {
return Err(Error::Custom(format!(
"clock skew too large, expected <= {MAX_CLOCK_SKEW_SECS} seconds, got {}",
opts.clock_skew_secs
)));
}
Ok(Validator { opts })
}
fn now(&self) -> i64 {
match self.opts.fixed_now {
Some(now) => now,
None => SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| to_secs(d.as_secs()))
.unwrap_or(0),
}
}
pub fn validate(&self, claims: &Claims) -> Result<(), Error> {
self.check_times(
claims.expiration.map(to_secs),
claims.not_before.map(to_secs),
claims.issued_at.map(to_secs),
)?;
self.check_identity(claims.issuer.as_deref(), claims.audience.as_deref())
}
pub fn validate_map(&self, claims: &ClaimsMap) -> Result<(), Error> {
self.check_times(
numeric_date_claim(claims, iana::CWTClaimExp)?,
numeric_date_claim(claims, iana::CWTClaimNbf)?,
numeric_date_claim(claims, iana::CWTClaimIat)?,
)?;
self.check_identity(
claims.get_text(iana::CWTClaimIss)?,
claims.get_text(iana::CWTClaimAud)?,
)
}
fn check_times(
&self,
exp: Option<i64>,
nbf: Option<i64>,
iat: Option<i64>,
) -> Result<(), Error> {
let now = self.now();
let skew = to_secs(self.opts.clock_skew_secs);
match exp {
None if !self.opts.allow_missing_expiration => {
return Err(Error::Custom("token doesn't have an expiration set".into()));
}
Some(exp) if exp <= now - skew => {
return Err(Error::Custom("token has expired".into()));
}
_ => {}
}
if let Some(nbf) = nbf {
if nbf > now + skew {
return Err(Error::Custom("token cannot be used yet".into()));
}
}
if self.opts.expect_issued_in_the_past {
if let Some(iat) = iat {
if iat > now + skew {
return Err(Error::Custom(
"token has an invalid iat claim in the future".into(),
));
}
}
}
Ok(())
}
fn check_identity(&self, issuer: Option<&str>, audience: Option<&str>) -> Result<(), Error> {
if let Some(expected) = &self.opts.expected_issuer {
if Some(expected.as_str()) != issuer {
return Err(Error::Custom(format!(
"issuer mismatch, expected {expected:?}, got {issuer:?}"
)));
}
}
if let Some(expected) = &self.opts.expected_audience {
if Some(expected.as_str()) != audience {
return Err(Error::Custom(format!(
"audience mismatch, expected {expected:?}, got {audience:?}"
)));
}
}
Ok(())
}
}