use std::collections::BTreeMap;
use cbor2::Cbor;
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Label {
Int(i64),
Text(String),
}
impl serde::Serialize for Label {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match self {
Self::Int(value) => serializer.serialize_i64(*value),
Self::Text(value) => serializer.serialize_str(value),
}
}
}
struct LabelVisitor;
impl<'de> serde::de::Visitor<'de> for LabelVisitor {
type Value = Label;
fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("an integer or text COSE label")
}
fn visit_i64<E: serde::de::Error>(self, value: i64) -> Result<Self::Value, E> {
Ok(Label::Int(value))
}
fn visit_i128<E: serde::de::Error>(self, value: i128) -> Result<Self::Value, E> {
i64::try_from(value)
.map(Label::Int)
.map_err(|_| E::custom("COSE integer label is out of i64 range"))
}
fn visit_u64<E: serde::de::Error>(self, value: u64) -> Result<Self::Value, E> {
i64::try_from(value)
.map(Label::Int)
.map_err(|_| E::custom("COSE integer label is out of i64 range"))
}
fn visit_u128<E: serde::de::Error>(self, value: u128) -> Result<Self::Value, E> {
i64::try_from(value)
.map(Label::Int)
.map_err(|_| E::custom("COSE integer label is out of i64 range"))
}
fn visit_str<E: serde::de::Error>(self, value: &str) -> Result<Self::Value, E> {
Ok(Label::Text(value.into()))
}
fn visit_string<E: serde::de::Error>(self, value: String) -> Result<Self::Value, E> {
Ok(Label::Text(value))
}
}
impl<'de> serde::Deserialize<'de> for Label {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
deserializer.deserialize_any(LabelVisitor)
}
}
#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(transparent)]
pub struct CoseMap(pub BTreeMap<Label, cbor2::Value>);
impl CoseMap {
fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
#[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", skip_serializing_if = "Option::is_none", default)]
pub expiration: Option<u64>,
#[cbor(key = 5)]
#[serde(rename = "nbf", skip_serializing_if = "Option::is_none", default)]
pub not_before: Option<u64>,
#[cbor(key = 6)]
#[serde(rename = "iat", 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)]
#[serde(skip_serializing_if = "CoseMap::is_empty", default)]
pub extra: CoseMap,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let claims = Claims {
issuer: Some("coap://as.example.com".into()),
subject: Some("erikw".into()),
audience: Some("coap://light.example.com".into()),
expiration: Some(1444064944),
not_before: Some(1443944944),
issued_at: Some(1443944944),
cwt_id: Some(vec![0x0b, 0x71]),
extra: CoseMap::default(),
};
let bytes = cbor2::to_canonical_vec(&claims)?;
assert_eq!(&bytes[..2], &[0xd8, 0x3d]); assert_eq!(
hex::encode(&bytes),
"d83da70175636f61703a2f2f61732e6578616d706c652e636f6d02656572696b77\
037818636f61703a2f2f6c696768742e6578616d706c652e636f6d041a5612aeb0\
051a5610d9f0061a5610d9f007420b71"
);
println!("{}", cbor2::diagnostic(&bytes[..])?);
let from_tagged: Claims = cbor2::from_slice(&bytes)?;
let from_untagged: Claims = cbor2::from_slice(&bytes[2..])?;
assert_eq!(from_tagged, claims);
assert_eq!(from_untagged, claims);
assert_eq!(Claims::TAG, Some(61));
assert_eq!(
Claims::KEYS,
&[
("iss", 1),
("sub", 2),
("aud", 3),
("exp", 4),
("nbf", 5),
("iat", 6),
("cti", 7),
]
);
let json = serde_json::to_string(&claims)?;
let from_json: Claims = serde_json::from_str(&json)?;
assert_eq!(from_json, claims);
println!("{json}");
let minimal = Claims {
issuer: Some("me".into()),
expiration: Some(1444064944),
..Default::default()
};
let minimal_bytes = cbor2::to_canonical_vec(&minimal)?;
println!("{}", cbor2::diagnostic(&minimal_bytes[..])?);
assert_eq!(
serde_json::to_string(&minimal)?,
r#"{"iss":"me","exp":1444064944}"#
);
let mut extended = minimal.clone();
extended
.extra
.0
.insert(Label::Text("tenant".into()), cbor2::Value::from("acme"));
let extended_bytes = cbor2::to_canonical_vec(&extended)?;
assert_eq!(cbor2::from_slice::<Claims>(&extended_bytes)?, extended);
assert_eq!(
serde_json::to_string(&extended)?,
r#"{"iss":"me","exp":1444064944,"tenant":"acme"}"#
);
let now = 1444060000; assert!(claims.expiration.is_some_and(|exp| exp > now)); Ok(())
}