From c28635dc65ecf584486d56d034fde300d0f5d29f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Rosales?= Date: Wed, 25 Dec 2024 23:22:40 -0600 Subject: [PATCH] fix(rust/signed_doc): refactor Metadata impl TryFrom --- rust/signed_doc/src/lib.rs | 213 ++++------------------------ rust/signed_doc/src/metadata/mod.rs | 167 ++++++++++++++++++++++ 2 files changed, 192 insertions(+), 188 deletions(-) diff --git a/rust/signed_doc/src/lib.rs b/rust/signed_doc/src/lib.rs index 891ae1d6e..201d12a35 100644 --- a/rust/signed_doc/src/lib.rs +++ b/rust/signed_doc/src/lib.rs @@ -10,17 +10,12 @@ use coset::{CborSerializable, TaggedCborSerializable}; mod metadata; -pub use metadata::{DocumentRef, Metadata}; +pub use metadata::{DocumentRef, Metadata, UuidV7}; /// Catalyst Signed Document Content Encoding Key. const CONTENT_ENCODING_KEY: &str = "content encoding"; /// Catalyst Signed Document Content Encoding Value. const CONTENT_ENCODING_VALUE: &str = "br"; -/// CBOR tag for UUID content. -const UUID_CBOR_TAG: u64 = 37; - -/// Collection of Content Errors. -pub struct ContentErrors(Vec); /// Keep all the contents private. /// Better even to use a structure like this. Wrapping in an Arc means we don't have to @@ -75,8 +70,26 @@ impl TryFrom> for CatalystSignedDocument { let cose = coset::CoseSign::from_tagged_slice(&cose_bytes) .or(coset::CoseSign::from_slice(&cose_bytes)) .map_err(|e| anyhow::anyhow!("Invalid COSE Sign document: {e}"))?; + let mut content_errors = Vec::new(); + let expected_header = cose_protected_header(); + + if cose.protected.header.content_type != expected_header.content_type { + content_errors + .push("Invalid COSE document protected header `content-type` field".to_string()); + } - let (metadata, content_errors) = metadata_from_cose_protected_header(&cose); + if !cose.protected.header.rest.iter().any(|(key, value)| { + key == &coset::Label::Text(CONTENT_ENCODING_KEY.to_string()) + && value == &coset::cbor::Value::Text(CONTENT_ENCODING_VALUE.to_string()) + }) { + content_errors.push( + "Invalid COSE document protected header {CONTENT_ENCODING_KEY} field".to_string(), + ); + } + let metadata = Metadata::from(&cose.protected); + if metadata.has_error() { + content_errors.extend_from_slice(metadata.content_errors()); + } let payload = match &cose.payload { Some(payload) => { let mut buf = Vec::new(); @@ -95,7 +108,7 @@ impl TryFrom> for CatalystSignedDocument { payload, signatures, cose_sign: cose, - content_errors: content_errors.0, + content_errors, }; Ok(CatalystSignedDocument { inner: Arc::new(inner), @@ -106,7 +119,7 @@ impl TryFrom> for CatalystSignedDocument { impl CatalystSignedDocument { // A bunch of getters to access the contents, or reason through the document, such as. - /// Are there any validation errors (as opposed to structural errors. + /// Are there any validation errors (as opposed to structural errors). #[must_use] pub fn has_error(&self) -> bool { !self.inner.content_errors.is_empty() @@ -166,38 +179,10 @@ fn cose_protected_header() -> coset::Header { .build() } -/// Decode `CBOR` encoded `UUID`. -fn decode_cbor_uuid(val: &coset::cbor::Value) -> anyhow::Result { - let Some((UUID_CBOR_TAG, coset::cbor::Value::Bytes(bytes))) = val.as_tag() else { - anyhow::bail!("Invalid CBOR encoded UUID type"); - }; - let uuid = uuid::Uuid::from_bytes( - bytes - .clone() - .try_into() - .map_err(|_| anyhow::anyhow!("Invalid CBOR encoded UUID type, invalid bytes size"))?, - ); - Ok(uuid) -} - -/// Decode `CBOR` encoded `DocumentRef`. -#[allow(clippy::indexing_slicing)] -fn decode_cbor_document_ref(val: &coset::cbor::Value) -> anyhow::Result { - if let Ok(id) = decode_cbor_uuid(val) { - Ok(DocumentRef::Latest { id }) - } else { - let Some(array) = val.as_array() else { - anyhow::bail!("Invalid CBOR encoded document `ref` type"); - }; - anyhow::ensure!(array.len() == 2, "Invalid CBOR encoded document `ref` type"); - let id = decode_cbor_uuid(&array[0])?; - let ver = decode_cbor_uuid(&array[1])?; - Ok(DocumentRef::WithVer(id, ver)) - } -} - /// Find a value for a given key in the protected header. -fn cose_protected_header_find(cose: &coset::CoseSign, rest_key: &str) -> Option { +fn cose_protected_header_find( + cose: &coset::CoseSign, rest_key: &str, +) -> Option { cose.protected .header .rest @@ -205,151 +190,3 @@ fn cose_protected_header_find(cose: &coset::CoseSign, rest_key: &str) -> Option< .find(|(key, _)| key == &coset::Label::Text(rest_key.to_string())) .map(|(_, value)| value.clone()) } - -/// Extract `Metadata` from `coset::CoseSign`. -#[allow(clippy::too_many_lines)] -fn metadata_from_cose_protected_header(cose: &coset::CoseSign) -> (Metadata, ContentErrors) { - let expected_header = cose_protected_header(); - let mut errors = Vec::new(); - - if cose.protected.header.content_type != expected_header.content_type { - errors.push("Invalid COSE document protected header `content-type` field".to_string()); - } - - if !cose.protected.header.rest.iter().any(|(key, value)| { - key == &coset::Label::Text(CONTENT_ENCODING_KEY.to_string()) - && value == &coset::cbor::Value::Text(CONTENT_ENCODING_VALUE.to_string()) - }) { - errors.push( - "Invalid COSE document protected header {CONTENT_ENCODING_KEY} field".to_string(), - ); - } - let mut metadata = Metadata::default(); - - match cose_protected_header_find(cose, "type") { - Some(doc_type) => { - match decode_cbor_uuid(&doc_type) { - Ok(doc_type_uuid) => { - if doc_type_uuid.get_version_num() == 4 { - metadata.r#type = doc_type_uuid; - } else { - errors.push(format!( - "Document type is not a valid UUIDv4: {doc_type_uuid}" - )); - } - }, - Err(e) => { - errors.push(format!( - "Invalid COSE protected header `type` field, err: {e}" - )); - }, - } - }, - None => errors.push("Invalid COSE protected header, missing `type` field".to_string()), - }; - - match cose_protected_header_find(cose, "id") { - Some(doc_id) => { - match decode_cbor_uuid(&doc_id) { - Ok(doc_id_uuid) => { - if doc_id_uuid.get_version_num() == 7 { - metadata.id = doc_id_uuid; - } else { - errors.push(format!("Document ID is not a valid UUIDv7: {doc_id_uuid}")); - } - }, - Err(e) => { - errors.push(format!( - "Invalid COSE protected header `id` field, err: {e}" - )); - }, - } - }, - None => errors.push("Invalid COSE protected header, missing `id` field".to_string()), - }; - - match cose_protected_header_find(cose, "ver") { - Some(doc_ver) => { - match decode_cbor_uuid(&doc_ver) { - Ok(doc_ver_uuid) => { - let mut is_valid = true; - if doc_ver_uuid.get_version_num() != 7 { - errors.push(format!( - "Document Version is not a valid UUIDv7: {doc_ver_uuid}" - )); - is_valid = false; - } - if doc_ver_uuid < metadata.id { - errors.push(format!( - "Document Version {doc_ver_uuid} cannot be smaller than Document ID {0}", metadata.id - )); - is_valid = false; - } - if is_valid { - metadata.ver = doc_ver_uuid; - } - }, - Err(e) => { - errors.push(format!( - "Invalid COSE protected header `ver` field, err: {e}" - )); - }, - } - }, - None => errors.push("Invalid COSE protected header, missing `ver` field".to_string()), - } - - if let Some(cbor_doc_ref) = cose_protected_header_find(cose, "ref") { - match decode_cbor_document_ref(&cbor_doc_ref) { - Ok(doc_ref) => { - metadata.r#ref = Some(doc_ref); - }, - Err(e) => { - errors.push(format!( - "Invalid COSE protected header `ref` field, err: {e}" - )); - }, - } - } - - if let Some(cbor_doc_template) = cose_protected_header_find(cose, "template") { - match decode_cbor_document_ref(&cbor_doc_template) { - Ok(doc_template) => { - metadata.template = Some(doc_template); - }, - Err(e) => { - errors.push(format!( - "Invalid COSE protected header `template` field, err: {e}" - )); - }, - } - } - - if let Some(cbor_doc_reply) = cose_protected_header_find(cose, "reply") { - match decode_cbor_document_ref(&cbor_doc_reply) { - Ok(doc_reply) => { - metadata.reply = Some(doc_reply); - }, - Err(e) => { - errors.push(format!( - "Invalid COSE protected header `reply` field, err: {e}" - )); - }, - } - } - - if let Some(cbor_doc_section) = cose_protected_header_find(cose, "section") { - match cbor_doc_section.into_text() { - Ok(doc_section) => { - metadata.section = Some(doc_section); - }, - Err(e) => { - errors.push(format!( - "Invalid COSE protected header `section` field, err: {e:?}" - )); - }, - } - } - - (metadata, ContentErrors(errors)) -} diff --git a/rust/signed_doc/src/metadata/mod.rs b/rust/signed_doc/src/metadata/mod.rs index 9823f91e7..a578cff47 100644 --- a/rust/signed_doc/src/metadata/mod.rs +++ b/rust/signed_doc/src/metadata/mod.rs @@ -24,6 +24,9 @@ pub struct Metadata { reply: Option, /// Reference to the document section. section: Option, + /// Metadata Content Errors + #[serde(skip)] + content_errors: Vec, } impl Metadata { @@ -68,6 +71,18 @@ impl Metadata { pub fn doc_section(&self) -> Option { self.section.clone() } + + /// Are there any validation errors (as opposed to structural errors). + #[must_use] + pub fn has_error(&self) -> bool { + !self.content_errors.is_empty() + } + + /// List of Content Errors. + #[must_use] + pub fn content_errors(&self) -> &Vec { + &self.content_errors + } } impl Display for Metadata { fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { @@ -93,6 +108,158 @@ impl Default for Metadata { template: None, reply: None, section: None, + content_errors: Vec::new(), + } + } +} + +/// Errors found when decoding content. +#[derive(Default, Debug)] +struct ContentErrors(Vec); + +impl ContentErrors { + /// Appends an element to the back of the collection + fn push(&mut self, error_string: String) { + self.0.push(error_string); + } +} + +impl From<&coset::ProtectedHeader> for Metadata { + #[allow(clippy::too_many_lines)] + fn from(protected: &coset::ProtectedHeader) -> Self { + let mut metadata = Metadata::default(); + let mut errors = Vec::new(); + + match cose_protected_header_find(protected, "type") { + Some(doc_type) => { + match UuidV4::try_from(&doc_type) { + Ok(doc_type_uuid) => { + metadata.r#type = doc_type_uuid; + }, + Err(e) => { + errors.push(format!("Document `type` is invalid: {e}")); + }, + } + }, + None => errors.push("Invalid COSE protected header, missing `type` field".to_string()), + }; + + match cose_protected_header_find(protected, "id") { + Some(doc_id) => { + match UuidV7::try_from(&doc_id) { + Ok(doc_id_uuid) => { + metadata.id = doc_id_uuid; + }, + Err(e) => { + errors.push(format!("Document `id` is invalid: {e}")); + }, + } + }, + None => errors.push("Invalid COSE protected header, missing `id` field".to_string()), + }; + + match cose_protected_header_find(protected, "ver") { + Some(doc_ver) => { + match UuidV7::try_from(&doc_ver) { + Ok(doc_ver_uuid) => { + if doc_ver_uuid < metadata.id { + errors.push(format!( + "Document Version {doc_ver_uuid} cannot be smaller than Document ID {}", metadata.id + )); + } else { + metadata.ver = doc_ver_uuid; + } + }, + Err(e) => { + errors.push(format!( + "Invalid COSE protected header `ver` field, err: {e}" + )); + }, + } + }, + None => errors.push("Invalid COSE protected header, missing `ver` field".to_string()), + } + + if let Some(cbor_doc_ref) = cose_protected_header_find(protected, "ref") { + match decode_cbor_document_ref(&cbor_doc_ref) { + Ok(doc_ref) => { + metadata.r#ref = Some(doc_ref); + }, + Err(e) => { + errors.push(format!( + "Invalid COSE protected header `ref` field, err: {e}" + )); + }, + } + } + + if let Some(cbor_doc_template) = cose_protected_header_find(protected, "template") { + match decode_cbor_document_ref(&cbor_doc_template) { + Ok(doc_template) => { + metadata.template = Some(doc_template); + }, + Err(e) => { + errors.push(format!( + "Invalid COSE protected header `template` field, err: {e}" + )); + }, + } + } + + if let Some(cbor_doc_reply) = cose_protected_header_find(protected, "reply") { + match decode_cbor_document_ref(&cbor_doc_reply) { + Ok(doc_reply) => { + metadata.reply = Some(doc_reply); + }, + Err(e) => { + errors.push(format!( + "Invalid COSE protected header `reply` field, err: {e}" + )); + }, + } } + + if let Some(cbor_doc_section) = cose_protected_header_find(protected, "section") { + match cbor_doc_section.into_text() { + Ok(doc_section) => { + metadata.section = Some(doc_section); + }, + Err(e) => { + errors.push(format!( + "Invalid COSE protected header `section` field, err: {e:?}" + )); + }, + } + } + metadata.content_errors = errors; + metadata + } +} + +/// Find a value for a given key in the protected header. +fn cose_protected_header_find( + protected: &coset::ProtectedHeader, rest_key: &str, +) -> Option { + protected + .header + .rest + .iter() + .find(|(key, _)| key == &coset::Label::Text(rest_key.to_string())) + .map(|(_, value)| value.clone()) +} + +/// Decode `CBOR` encoded `DocumentRef`. +#[allow(clippy::indexing_slicing)] +fn decode_cbor_document_ref(val: &coset::cbor::Value) -> anyhow::Result { + if let Ok(id) = UuidV7::try_from(val) { + Ok(DocumentRef::Latest { id }) + } else { + let Some(array) = val.as_array() else { + anyhow::bail!("Invalid CBOR encoded document `ref` type"); + }; + anyhow::ensure!(array.len() == 2, "Invalid CBOR encoded document `ref` type"); + let id = UuidV7::try_from(&array[0])?; + let ver = UuidV7::try_from(&array[1])?; + Ok(DocumentRef::WithVer(id, ver)) } }