blob: d12531afd20d69e6b427c4d8cdd52bf5b178cb58 [file] [log] [blame]
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
////////////////////////////////////////////////////////////////////////////////
//! CBOR Web Token functionality.
use crate::{
cbor::value::Value,
common::AsCborValue,
iana,
iana::{EnumI64, WithPrivateRange},
util::{cbor_type_error, ValueTryAs},
CoseError,
};
use alloc::{collections::BTreeSet, string::String, vec::Vec};
use core::convert::TryInto;
#[cfg(test)]
mod tests;
/// Number of seconds since UNIX epoch.
#[derive(Clone, Debug, PartialEq)]
pub enum Timestamp {
WholeSeconds(i64),
FractionalSeconds(f64),
}
impl AsCborValue for Timestamp {
fn from_cbor_value(value: Value) -> Result<Self, CoseError> {
match value {
Value::Integer(i) => Ok(Timestamp::WholeSeconds(i.try_into()?)),
Value::Float(f) => Ok(Timestamp::FractionalSeconds(f)),
_ => cbor_type_error(&value, "int/float"),
}
}
fn to_cbor_value(self) -> Result<Value, CoseError> {
Ok(match self {
Timestamp::WholeSeconds(t) => Value::Integer(t.into()),
Timestamp::FractionalSeconds(f) => Value::Float(f),
})
}
}
/// Claim name.
pub type ClaimName = crate::RegisteredLabelWithPrivate<iana::CwtClaimName>;
/// Structure representing a CWT Claims Set.
#[derive(Clone, Debug, Default, PartialEq)]
pub struct ClaimsSet {
/// Issuer
pub issuer: Option<String>,
/// Subject
pub subject: Option<String>,
/// Audience
pub audience: Option<String>,
/// Expiration Time
pub expiration_time: Option<Timestamp>,
/// Not Before
pub not_before: Option<Timestamp>,
/// Issued At
pub issued_at: Option<Timestamp>,
/// CWT ID
pub cwt_id: Option<Vec<u8>>,
/// Any additional claims.
pub rest: Vec<(ClaimName, Value)>,
}
impl crate::CborSerializable for ClaimsSet {}
const ISS: ClaimName = ClaimName::Assigned(iana::CwtClaimName::Iss);
const SUB: ClaimName = ClaimName::Assigned(iana::CwtClaimName::Sub);
const AUD: ClaimName = ClaimName::Assigned(iana::CwtClaimName::Aud);
const EXP: ClaimName = ClaimName::Assigned(iana::CwtClaimName::Exp);
const NBF: ClaimName = ClaimName::Assigned(iana::CwtClaimName::Nbf);
const IAT: ClaimName = ClaimName::Assigned(iana::CwtClaimName::Iat);
const CTI: ClaimName = ClaimName::Assigned(iana::CwtClaimName::Cti);
impl AsCborValue for ClaimsSet {
fn from_cbor_value(value: Value) -> Result<Self, CoseError> {
let m = match value {
Value::Map(m) => m,
v => return cbor_type_error(&v, "map"),
};
let mut claims = Self::default();
let mut seen = BTreeSet::new();
for (n, value) in m.into_iter() {
// The `ciborium` CBOR library does not police duplicate map keys, so do it here.
let name = ClaimName::from_cbor_value(n)?;
if seen.contains(&name) {
return Err(CoseError::DuplicateMapKey);
}
seen.insert(name.clone());
match name {
x if x == ISS => claims.issuer = Some(value.try_as_string()?),
x if x == SUB => claims.subject = Some(value.try_as_string()?),
x if x == AUD => claims.audience = Some(value.try_as_string()?),
x if x == EXP => claims.expiration_time = Some(Timestamp::from_cbor_value(value)?),
x if x == NBF => claims.not_before = Some(Timestamp::from_cbor_value(value)?),
x if x == IAT => claims.issued_at = Some(Timestamp::from_cbor_value(value)?),
x if x == CTI => claims.cwt_id = Some(value.try_as_bytes()?),
name => claims.rest.push((name, value)),
}
}
Ok(claims)
}
fn to_cbor_value(self) -> Result<Value, CoseError> {
let mut map = Vec::new();
if let Some(iss) = self.issuer {
map.push((ISS.to_cbor_value()?, Value::Text(iss)));
}
if let Some(sub) = self.subject {
map.push((SUB.to_cbor_value()?, Value::Text(sub)));
}
if let Some(aud) = self.audience {
map.push((AUD.to_cbor_value()?, Value::Text(aud)));
}
if let Some(exp) = self.expiration_time {
map.push((EXP.to_cbor_value()?, exp.to_cbor_value()?));
}
if let Some(nbf) = self.not_before {
map.push((NBF.to_cbor_value()?, nbf.to_cbor_value()?));
}
if let Some(iat) = self.issued_at {
map.push((IAT.to_cbor_value()?, iat.to_cbor_value()?));
}
if let Some(cti) = self.cwt_id {
map.push((CTI.to_cbor_value()?, Value::Bytes(cti)));
}
for (label, value) in self.rest {
map.push((label.to_cbor_value()?, value));
}
Ok(Value::Map(map))
}
}
/// Builder for [`ClaimsSet`] objects.
#[derive(Default)]
pub struct ClaimsSetBuilder(ClaimsSet);
impl ClaimsSetBuilder {
builder! {ClaimsSet}
builder_set_optional! {issuer: String}
builder_set_optional! {subject: String}
builder_set_optional! {audience: String}
builder_set_optional! {expiration_time: Timestamp}
builder_set_optional! {not_before: Timestamp}
builder_set_optional! {issued_at: Timestamp}
builder_set_optional! {cwt_id: Vec<u8>}
/// Set a claim name:value pair.
///
/// # Panics
///
/// This function will panic if it used to set a claim with name from the range [1, 7].
#[must_use]
pub fn claim(mut self, name: iana::CwtClaimName, value: Value) -> Self {
if name.to_i64() >= iana::CwtClaimName::Iss.to_i64()
&& name.to_i64() <= iana::CwtClaimName::Cti.to_i64()
{
panic!("claim() method used to set core claim"); // safe: invalid input
}
self.0.rest.push((ClaimName::Assigned(name), value));
self
}
/// Set a claim name:value pair where the `name` is text.
#[must_use]
pub fn text_claim(mut self, name: String, value: Value) -> Self {
self.0.rest.push((ClaimName::Text(name), value));
self
}
/// Set a claim where the claim key is a numeric value from the private use range.
///
/// # Panics
///
/// This function will panic if it is used to set a claim with a key value outside of the
/// private use range.
#[must_use]
pub fn private_claim(mut self, id: i64, value: Value) -> Self {
assert!(iana::CwtClaimName::is_private(id));
self.0.rest.push((ClaimName::PrivateUse(id), value));
self
}
}