pub mod data_retrieval;
pub mod issue;
pub mod mdl;
use std::str::FromStr;
use bherror::traits::{ErrorContext as _, ForeignError as _};
use chrono::{Timelike as _, Utc};
use ciborium::{from_reader, into_writer, value::Value};
pub use data_retrieval::{
common::NameSpace,
device_retrieval::{
reader_auth::ReaderAuth,
request::{DeviceRequest, DocRequest, IntentToRetain},
response::DeviceResponse,
},
Claims,
};
use hex::FromHexError;
use rand::Rng;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use crate::{utils::rand::generate_salt, MdocError};
const MDOC_FULL_DATE_CBOR_TAG: u64 = 1004;
const MDOC_TDATE_CBOR_TAG: u64 = 0;
const MDOC_BYTES_CBOR_TAG: u64 = 24;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(into = "Value")]
pub struct Bytes(Vec<u8>);
impl Bytes {
pub fn from_hex(value: &str) -> Result<Self, FromHexError> {
let value = hex::decode(value)?;
Ok(Self(value))
}
pub fn random_salt<R: Rng + ?Sized>(rng: &mut R) -> Self {
let salt = generate_salt(rng);
Self(salt)
}
}
impl FromStr for Bytes {
type Err = FromHexError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::from_hex(value)
}
}
impl From<Vec<u8>> for Bytes {
fn from(bytes: Vec<u8>) -> Self {
Self(bytes)
}
}
impl From<Bytes> for Value {
fn from(bytes: Bytes) -> Self {
Self::Bytes(bytes.0)
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct BytesCbor<T> {
pub(crate) inner: T,
pub(crate) original_data: Option<Vec<u8>>,
}
impl<T> BytesCbor<T> {
pub fn try_from_cbor(value: &Value) -> Result<Self, String>
where
T: serde::de::DeserializeOwned,
{
let tagged_value @ Value::Tag(MDOC_BYTES_CBOR_TAG, ref value) = value else {
return Err(format!(
"`bstr .cbor` MUST be tagged with `{}`",
MDOC_BYTES_CBOR_TAG
));
};
let bytes = value
.as_bytes()
.ok_or_else(|| "`bstr .cbor` MUST be `Bytes`".to_owned())?;
let inner = from_reader(bytes.as_slice()).map_err(|err| err.to_string())?;
let mut original_data = Vec::new();
into_writer(tagged_value, &mut original_data).map_err(|err| err.to_string())?;
Ok(Self {
inner,
original_data: Some(original_data),
})
}
pub fn try_into_cbor(&self) -> Result<Value, ciborium::ser::Error<std::io::Error>>
where
T: Serialize,
{
let bytes = match self.original_data {
Some(ref bytes) => return Ok(from_reader(bytes.as_slice()).unwrap()),
None => {
let mut bytes = vec![];
into_writer(&self.inner, &mut bytes)?;
bytes
}
};
let bytes = Value::Bytes(bytes);
let tag = Value::Tag(MDOC_BYTES_CBOR_TAG, Box::new(bytes));
Ok(tag)
}
}
impl<T> From<T> for BytesCbor<T> {
fn from(value: T) -> Self {
Self {
inner: value,
original_data: None,
}
}
}
impl<T> Serialize for BytesCbor<T>
where
T: Serialize,
{
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let value = self.try_into_cbor().map_err(serde::ser::Error::custom)?;
value.serialize(serializer)
}
}
impl<'de, T> Deserialize<'de> for BytesCbor<T>
where
T: serde::de::DeserializeOwned,
{
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value = Value::deserialize(deserializer)?;
Self::try_from_cbor(&value).map_err(serde::de::Error::custom)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(into = "Value", try_from = "Value")]
pub struct DateTime(chrono::DateTime<Utc>);
impl FromStr for DateTime {
type Err = bherror::Error<MdocError>;
fn from_str(value: &str) -> Result<Self, Self::Err> {
let date_time = chrono::DateTime::parse_from_rfc3339(value)
.foreign_err(|| MdocError::InvalidDateTime)
.ctx(|| format!("{value} not a valid Date Time string"))?;
if date_time.offset().utc_minus_local() != 0 {
return Err(bherror::Error::root(MdocError::InvalidDateTime)
.ctx("Date Time is not in UTC (offset must be Z)"));
}
let date_time = date_time.with_timezone(&Utc);
DateTime::try_from(date_time)
}
}
impl TryFrom<u64> for DateTime {
type Error = bherror::Error<MdocError>;
fn try_from(value: u64) -> Result<Self, Self::Error> {
let value_i64 = value
.try_into()
.foreign_err(|| MdocError::InvalidDateTime)
.ctx(|| format!("{value} seconds do not fit into i64"))?;
let date_time = chrono::DateTime::from_timestamp(value_i64, 0).ok_or_else(|| {
bherror::Error::root(MdocError::InvalidDateTime)
.ctx(format!("{value} seconds out of range"))
})?;
DateTime::try_from(date_time)
}
}
impl From<DateTime> for Value {
fn from(date_time: DateTime) -> Self {
let date_time = date_time
.0
.to_rfc3339_opts(chrono::SecondsFormat::Secs, true);
Self::Tag(MDOC_TDATE_CBOR_TAG, Box::new(Self::Text(date_time)))
}
}
impl TryFrom<Value> for DateTime {
type Error = bherror::Error<MdocError>;
fn try_from(value: Value) -> Result<Self, Self::Error> {
let Value::Tag(MDOC_TDATE_CBOR_TAG, value) = value else {
return Err(
bherror::Error::root(MdocError::InvalidDateTime).ctx(format!(
"`tdate` MUST be tagged with `{}`",
MDOC_TDATE_CBOR_TAG
)),
);
};
let value = value.as_text().ok_or_else(|| {
bherror::Error::root(MdocError::InvalidDateTime).ctx("`tdate` MUST be `String`")
})?;
value.parse::<DateTime>()
}
}
impl TryFrom<chrono::DateTime<Utc>> for DateTime {
type Error = bherror::Error<MdocError>;
fn try_from(value: chrono::DateTime<Utc>) -> Result<Self, Self::Error> {
if value.nanosecond() != 0 {
return Err(bherror::Error::root(MdocError::InvalidDateTime)
.ctx("Date Time should not use fraction of seconds"));
}
Ok(Self(value))
}
}
impl From<DateTime> for chrono::DateTime<Utc> {
fn from(date_time: DateTime) -> Self {
date_time.0
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(into = "Value", try_from = "Value")]
pub struct FullDate(chrono::NaiveDate);
impl FromStr for FullDate {
type Err = chrono::ParseError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Ok(Self(chrono::NaiveDate::parse_from_str(value, "%Y-%m-%d")?))
}
}
impl From<FullDate> for Value {
fn from(full_date: FullDate) -> Self {
let text = full_date.0.format("%Y-%m-%d").to_string();
let text = Self::Text(text);
Self::Tag(MDOC_FULL_DATE_CBOR_TAG, Box::new(text))
}
}
impl TryFrom<Value> for FullDate {
type Error = String;
fn try_from(value: Value) -> Result<Self, Self::Error> {
let Value::Tag(MDOC_FULL_DATE_CBOR_TAG, value) = value else {
return Err(format!(
"`full-date` MUST be tagged with `{}`",
MDOC_FULL_DATE_CBOR_TAG
));
};
value
.as_text()
.ok_or_else(|| "`full-date` MUST be `String`".to_owned())?
.parse()
.map_err(|err: chrono::ParseError| err.to_string())
}
}
#[cfg(test)]
mod tests {
use assert_matches::assert_matches;
use ciborium::{from_reader, into_writer};
use super::*;
#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct Vehicle {
vehicle_category_code: String,
issue_date: FullDate,
expiry_date: FullDate,
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct Vehicles {
vehicles: Vec<Vehicle>,
}
#[test]
fn test_cbor() {
const EXPECTED_CBOR: &str = "\
82a37576656869636c655f63617465676f72795f636f646561416a69737375655f64617465d903ec6a32303138\
2d30382d30396b6578706972795f64617465d903ec6a323032342d31302d3230a37576656869636c655f636174\
65676f72795f636f646561426a69737375655f64617465d903ec6a323031372d30322d32336b6578706972795f\
64617465d903ec6a323032342d31302d3230";
let model = Vehicles {
vehicles: vec![
Vehicle {
vehicle_category_code: "A".to_owned(),
issue_date: "2018-08-09".parse().unwrap(),
expiry_date: "2024-10-20".parse().unwrap(),
},
Vehicle {
vehicle_category_code: "B".to_owned(),
issue_date: "2017-02-23".parse().unwrap(),
expiry_date: "2024-10-20".parse().unwrap(),
},
],
};
let mut encoded = Vec::new();
into_writer(&model, &mut encoded).unwrap();
let encoded_hex = hex::encode(&encoded);
assert_eq!(EXPECTED_CBOR, encoded_hex);
let decoded: Vehicles = from_reader(encoded.as_slice()).unwrap();
assert_eq!(model, decoded);
}
#[test]
fn test_datetime_success() {
const EXPECTED_CBOR: &str = "c074323032302d31302d30315431333a33303a30325a";
let date_time: DateTime = "2020-10-01T13:30:02Z".parse().unwrap();
let mut encoded = Vec::new();
into_writer(&date_time, &mut encoded).unwrap();
let encoded_hex = hex::encode(&encoded);
assert_eq!(EXPECTED_CBOR, encoded_hex);
let decoded: DateTime = from_reader(encoded.as_slice()).unwrap();
assert_eq!(date_time, decoded);
}
#[test]
fn test_datetime_sub_secs_fails() {
let dt = "1985-04-12T23:20:50Z";
let _date_time: DateTime = dt.parse().unwrap();
let _date_time: DateTime =
Value::Tag(MDOC_TDATE_CBOR_TAG, Box::new(Value::Text(dt.to_owned())))
.try_into()
.unwrap();
let dt = "1985-04-12T23:20:50.52Z";
let err = dt.parse::<DateTime>().unwrap_err();
assert_matches!(err.error, MdocError::InvalidDateTime);
let err = DateTime::try_from(Value::Tag(
MDOC_TDATE_CBOR_TAG,
Box::new(Value::Text(dt.to_owned())),
))
.unwrap_err();
assert_matches!(err.error, MdocError::InvalidDateTime);
}
#[test]
fn test_datetime_non_utc_fails() {
let dt = "1996-12-19T16:39:57Z";
let _date_time: DateTime = dt.parse().unwrap();
let _date_time: DateTime =
Value::Tag(MDOC_TDATE_CBOR_TAG, Box::new(Value::Text(dt.to_owned())))
.try_into()
.unwrap();
let dt = "1996-12-19T16:39:57-08:00";
let err = dt.parse::<DateTime>().unwrap_err();
assert_matches!(err.error, MdocError::InvalidDateTime);
let err = DateTime::try_from(Value::Tag(
MDOC_TDATE_CBOR_TAG,
Box::new(Value::Text(dt.to_owned())),
))
.unwrap_err();
assert_matches!(err.error, MdocError::InvalidDateTime);
}
#[test]
fn test_cbor_tdate_untagged_fails() {
const THIRD_PARTY_TDATE_CBOR: &str = "74323032302d31302d30315431333a33303a30325a";
let data = hex::decode(THIRD_PARTY_TDATE_CBOR).unwrap();
let err = from_reader::<DateTime, _>(data.as_slice()).unwrap_err();
assert_matches!(err, ciborium::de::Error::Semantic(None, m) if m == "Invalid value for Date Time");
}
#[test]
fn test_value_tdate_untagged_fails() {
let data = ciborium::Value::Text("2020-10-01T13:30:02Z".to_owned());
let err = DateTime::try_from(data).unwrap_err();
assert_matches!(err.error, MdocError::InvalidDateTime);
}
}