diff --git a/rust/signed_doc/Cargo.toml b/rust/signed_doc/Cargo.toml index ccaa9ee77d..a7eaaf59dc 100644 --- a/rust/signed_doc/Cargo.toml +++ b/rust/signed_doc/Cargo.toml @@ -21,7 +21,7 @@ brotli = "7.0.0" ed25519-dalek = { version = "2.1.1", features = ["pem", "rand_core"] } uuid = { version = "1.11.0", features = ["v4", "v7", "serde"] } hex = "0.4.3" -thiserror = "2.0.9" +strum = { version = "0.26.3", features = ["derive"] } [dev-dependencies] clap = { version = "4.5.23", features = ["derive", "env"] } diff --git a/rust/signed_doc/examples/mk_signed_doc.rs b/rust/signed_doc/examples/mk_signed_doc.rs index a916cbfc64..4dfc0af6e0 100644 --- a/rust/signed_doc/examples/mk_signed_doc.rs +++ b/rust/signed_doc/examples/mk_signed_doc.rs @@ -8,7 +8,7 @@ use std::{ path::PathBuf, }; -use catalyst_signed_doc::{Builder, CatalystSignedDocument, Decode, Decoder, KidUri, Metadata}; +use catalyst_signed_doc::{Builder, CatalystSignedDocument, KidUri, Metadata}; use clap::Parser; use coset::CborSerializable; use ed25519_dalek::{ed25519::signature::Signer, pkcs8::DecodePrivateKey}; @@ -107,7 +107,8 @@ fn decode_signed_doc(cose_bytes: &[u8]) { cose_bytes.len(), hex::encode(cose_bytes) ); - match CatalystSignedDocument::decode(&mut Decoder::new(cose_bytes), &mut ()) { + + match CatalystSignedDocument::try_from(cose_bytes) { Ok(cat_signed_doc) => { println!("This is a valid Catalyst Document."); println!("{cat_signed_doc}"); diff --git a/rust/signed_doc/src/error.rs b/rust/signed_doc/src/error.rs index a58ff104e3..5bea115736 100644 --- a/rust/signed_doc/src/error.rs +++ b/rust/signed_doc/src/error.rs @@ -1,29 +1,48 @@ -//! Catalyst Signed Document errors. +//! Catalyst Signed Document Error -/// Catalyst Signed Document error. -#[derive(thiserror::Error, Debug)] -#[error("Catalyst Signed Document Error: {0:?}")] -pub struct Error(pub(crate) List); +use std::fmt; -/// List of errors. +use catalyst_types::problem_report::ProblemReport; + +/// Catalyst Signed Document Error +#[allow(clippy::module_name_repetitions)] #[derive(Debug)] -pub(crate) struct List(pub(crate) Vec); +pub struct CatalystSignedDocError { + /// List of errors during processing. + report: ProblemReport, + /// Actual error. + error: anyhow::Error, +} -impl From> for List { - fn from(e: Vec) -> Self { - Self(e) +impl CatalystSignedDocError { + /// Create a new `CatalystSignedDocError`. + #[must_use] + pub fn new(report: ProblemReport, error: anyhow::Error) -> Self { + Self { report, error } } -} -impl From> for Error { - fn from(e: Vec) -> Self { - Self(e.into()) + /// Get the error report. + #[must_use] + pub fn report(&self) -> &ProblemReport { + &self.report + } + + /// Get the actual error. + #[must_use] + pub fn error(&self) -> &anyhow::Error { + &self.error } } -impl Error { - /// List of errors. - pub fn errors(&self) -> &Vec { - &self.0 .0 +impl fmt::Display for CatalystSignedDocError { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + let report_json = serde_json::to_string(&self.report) + .unwrap_or_else(|_| String::from("Failed to serialize ProblemReport")); + + write!( + fmt, + "CatalystSignedDocError {{ error: {}, report: {} }}", + self.error, report_json + ) } } diff --git a/rust/signed_doc/src/lib.rs b/rust/signed_doc/src/lib.rs index 109fc3ac3a..24cc5d10dc 100644 --- a/rust/signed_doc/src/lib.rs +++ b/rust/signed_doc/src/lib.rs @@ -2,9 +2,10 @@ mod builder; mod content; -mod error; +pub mod error; mod metadata; mod signature; +mod utils; use std::{ convert::TryFrom, @@ -12,13 +13,15 @@ use std::{ sync::Arc, }; -use anyhow::anyhow; pub use builder::Builder; +use catalyst_types::problem_report::ProblemReport; pub use content::Content; use coset::{CborSerializable, Header}; +use error::CatalystSignedDocError; pub use metadata::{DocumentRef, ExtraFields, Metadata, UuidV4, UuidV7}; pub use minicbor::{decode, encode, Decode, Decoder, Encode}; pub use signature::{KidUri, Signatures}; +use utils::context::DecodeSignDocCtx; /// Inner type that holds the Catalyst Signed Document with parsing errors. #[derive(Debug, Clone)] @@ -104,8 +107,20 @@ impl CatalystSignedDocument { } } -impl Decode<'_, ()> for CatalystSignedDocument { - fn decode(d: &mut Decoder<'_>, (): &mut ()) -> Result { +impl TryFrom<&[u8]> for CatalystSignedDocument { + type Error = CatalystSignedDocError; + + fn try_from(value: &[u8]) -> Result { + let error_report = ProblemReport::new("Catalyst Signed Document"); + let mut ctx = DecodeSignDocCtx { error_report }; + let decoded: CatalystSignedDocument = minicbor::decode_with(value, &mut ctx) + .map_err(|e| CatalystSignedDocError::new(ctx.error_report, e.into()))?; + Ok(decoded) + } +} + +impl Decode<'_, DecodeSignDocCtx> for CatalystSignedDocument { + fn decode(d: &mut Decoder<'_>, ctx: &mut DecodeSignDocCtx) -> Result { let start = d.position(); d.skip()?; let end = d.position(); @@ -115,40 +130,67 @@ impl Decode<'_, ()> for CatalystSignedDocument { .ok_or(minicbor::decode::Error::end_of_input())?; let cose_sign = coset::CoseSign::from_slice(cose_bytes).map_err(|e| { + ctx.error_report.invalid_value( + "COSE sign document bytes", + &format!("{:?}", &cose_bytes), + &format!("Cannot convert bytes to CoseSign {e:?}"), + "Creating COSE Sign document", + ); minicbor::decode::Error::message(format!("Invalid COSE Sign document: {e}")) })?; - let mut errors = Vec::new(); - - let metadata = Metadata::try_from(&cose_sign.protected).map_or_else( - |e| { - errors.extend(e.0 .0); - None - }, - Some, - ); - let signatures = Signatures::try_from(&cose_sign.signatures).map_or_else( - |e| { - errors.extend(e.0 .0); - None - }, - Some, - ); + let metadata = Metadata::from_protected_header(&cose_sign.protected, &ctx.error_report) + .map_or_else( + |e| { + ctx.error_report.conversion_error( + "COSE sign protected header", + &format!("{:?}", &cose_sign.protected), + &format!("Expected Metadata: {e:?}"), + "Converting COSE Sign protected header to Metadata", + ); + None + }, + Some, + ); + let signatures = Signatures::from_cose_sig(&cose_sign.signatures, &ctx.error_report) + .map_or_else( + |e| { + ctx.error_report.conversion_error( + "COSE sign signatures", + &format!("{:?}", &cose_sign.signatures), + &format!("Expected Signatures {e:?}"), + "Converting COSE Sign signatures to Signatures", + ); + None + }, + Some, + ); if cose_sign.payload.is_none() { - errors.push(anyhow!("Document Content is missing")); + ctx.error_report + .missing_field("COSE Sign Payload", "Missing document content (payload)"); } match (cose_sign.payload, metadata, signatures) { (Some(payload), Some(metadata), Some(signatures)) => { let content = Content::from_encoded( - payload, + payload.clone(), metadata.content_type(), metadata.content_encoding(), ) .map_err(|e| { - errors.push(anyhow!("Invalid Document Content: {e}")); - minicbor::decode::Error::message(error::Error::from(errors)) + ctx.error_report.invalid_value( + "Document Content", + &format!( + "Given value {:?}, {:?}, {:?}", + payload, + metadata.content_type(), + metadata.content_encoding() + ), + &format!("{e:?}"), + "Creating document content", + ); + minicbor::decode::Error::message("Failed to create Document Content") })?; Ok(InnerCatalystSignedDocument { @@ -158,7 +200,11 @@ impl Decode<'_, ()> for CatalystSignedDocument { } .into()) }, - _ => Err(minicbor::decode::Error::message(error::Error::from(errors))), + _ => { + Err(minicbor::decode::Error::message( + "Failed to decode Catalyst Signed Document", + )) + }, } } } @@ -238,8 +284,8 @@ mod tests { let mut bytes = Vec::new(); minicbor::encode_with(doc, &mut bytes, &mut ()).unwrap(); - let decoded: CatalystSignedDocument = - minicbor::decode_with(bytes.as_slice(), &mut ()).unwrap(); + + let decoded: CatalystSignedDocument = bytes.as_slice().try_into().unwrap(); assert_eq!(decoded.doc_type(), uuid_v4); assert_eq!(decoded.doc_id(), uuid_v7); diff --git a/rust/signed_doc/src/metadata/algorithm.rs b/rust/signed_doc/src/metadata/algorithm.rs index ca5f1f7df3..731aab67fa 100644 --- a/rust/signed_doc/src/metadata/algorithm.rs +++ b/rust/signed_doc/src/metadata/algorithm.rs @@ -1,7 +1,9 @@ //! Cryptographic Algorithm in COSE SIGN protected header. +use strum::VariantArray; + /// Cryptography Algorithm. -#[derive(Copy, Clone, Debug, PartialEq, serde::Deserialize)] +#[derive(Copy, Clone, Debug, PartialEq, serde::Deserialize, VariantArray)] pub enum Algorithm { /// `EdDSA` EdDSA, @@ -25,7 +27,12 @@ impl TryFrom for Algorithm { fn try_from(value: coset::iana::Algorithm) -> Result { match value { coset::iana::Algorithm::EdDSA => Ok(Self::EdDSA), - _ => anyhow::bail!("Unsupported algorithm: {value:?}"), + _ => { + anyhow::bail!( + "Unsupported algorithm: {value:?}, Supported only: {:?}", + Algorithm::VARIANTS + ) + }, } } } diff --git a/rust/signed_doc/src/metadata/content_type.rs b/rust/signed_doc/src/metadata/content_type.rs index b45105a04e..40239692ce 100644 --- a/rust/signed_doc/src/metadata/content_type.rs +++ b/rust/signed_doc/src/metadata/content_type.rs @@ -7,9 +7,10 @@ use std::{ use coset::iana::CoapContentFormat; use serde::{de, Deserialize, Deserializer}; +use strum::VariantArray; /// Payload Content Type. -#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, VariantArray)] pub enum ContentType { /// 'application/cbor' Cbor, @@ -33,7 +34,12 @@ impl FromStr for ContentType { match s { "cbor" => Ok(Self::Cbor), "json" => Ok(Self::Json), - _ => anyhow::bail!("Unsupported Content Type: {s:?}"), + _ => { + anyhow::bail!( + "Unsupported Content Type: {s:?}, Supported only: {:?}", + ContentType::VARIANTS + ) + }, } } } @@ -62,7 +68,12 @@ impl TryFrom<&coset::ContentType> for ContentType { let content_type = match value { coset::ContentType::Assigned(CoapContentFormat::Json) => ContentType::Json, coset::ContentType::Assigned(CoapContentFormat::Cbor) => ContentType::Cbor, - _ => anyhow::bail!("Unsupported Content Type {value:?}"), + _ => { + anyhow::bail!( + "Unsupported Content Type {value:?}, Supported only: {:?}", + ContentType::VARIANTS + ) + }, }; Ok(content_type) } diff --git a/rust/signed_doc/src/metadata/extra_fields.rs b/rust/signed_doc/src/metadata/extra_fields.rs index 2332e2b274..6aa4a87533 100644 --- a/rust/signed_doc/src/metadata/extra_fields.rs +++ b/rust/signed_doc/src/metadata/extra_fields.rs @@ -1,6 +1,7 @@ //! Catalyst Signed Document Extra Fields. -use anyhow::anyhow; +use anyhow::bail; +use catalyst_types::problem_report::ProblemReport; use coset::{cbor::Value, Label, ProtectedHeader}; use super::{cose_protected_header_find, decode_cbor_uuid, encode_cbor_uuid, DocumentRef, UuidV4}; @@ -157,15 +158,16 @@ impl ExtraFields { } Ok(builder) } -} - -impl TryFrom<&ProtectedHeader> for ExtraFields { - type Error = crate::error::Error; + /// Converting COSE Protected Header to `ExtraFields`. #[allow(clippy::too_many_lines)] - fn try_from(protected: &ProtectedHeader) -> Result { + pub(crate) fn from_protected_header( + protected: &ProtectedHeader, error_report: &ProblemReport, + ) -> anyhow::Result { + /// Context for error messages. + const CONTEXT: &str = "COSE ProtectedHeader to ExtraFields"; + let mut extra = ExtraFields::default(); - let mut errors = Vec::new(); if let Some(cbor_doc_ref) = cose_protected_header_find(protected, |key| key == &Label::Text(REF_KEY.to_string())) @@ -175,9 +177,12 @@ impl TryFrom<&ProtectedHeader> for ExtraFields { extra.doc_ref = Some(doc_ref); }, Err(e) => { - errors.push(anyhow!( - "Invalid COSE protected header `ref` field, err: {e}" - )); + error_report.conversion_error( + "CBOR COSE protected header doc ref", + &format!("{cbor_doc_ref:?}"), + &format!("Expected DocumentRef: {e}"), + &format!("{CONTEXT}, DocumentRef"), + ); }, } } @@ -190,9 +195,12 @@ impl TryFrom<&ProtectedHeader> for ExtraFields { extra.template = Some(doc_template); }, Err(e) => { - errors.push(anyhow!( - "Invalid COSE protected header `template` field, err: {e}" - )); + error_report.conversion_error( + "CBOR COSE protected header document template", + &format!("{cbor_doc_template:?}"), + &format!("Expected DocumentRef: {e}"), + &format!("{CONTEXT}, DocumentRef"), + ); }, } } @@ -205,9 +213,12 @@ impl TryFrom<&ProtectedHeader> for ExtraFields { extra.reply = Some(doc_reply); }, Err(e) => { - errors.push(anyhow!( - "Invalid COSE protected header `reply` field, err: {e}" - )); + error_report.conversion_error( + "CBOR COSE protected header document reply", + &format!("{cbor_doc_reply:?}"), + &format!("Expected DocumentRef: {e}"), + &format!("{CONTEXT}, DocumentRef"), + ); }, } } @@ -220,9 +231,12 @@ impl TryFrom<&ProtectedHeader> for ExtraFields { extra.section = Some(doc_section); }, Err(e) => { - errors.push(anyhow!( - "Invalid COSE protected header `section` field, err: {e:?}" - )); + error_report.conversion_error( + "COSE protected header document section", + &format!("{cbor_doc_section:?}"), + &format!("Expected String: {e:?}"), + &format!("{CONTEXT}, converting document section to String"), + ); }, } } @@ -234,23 +248,29 @@ impl TryFrom<&ProtectedHeader> for ExtraFields { Ok(collabs) => { let mut c = Vec::new(); for (ids, collaborator) in collabs.iter().cloned().enumerate() { - match collaborator.into_text() { + match collaborator.clone().into_text() { Ok(collaborator) => { c.push(collaborator); }, Err(e) => { - errors.push(anyhow!( - "Invalid Collaborator at index {ids} of COSE protected header `collabs` field, err: {e:?}" - )); + error_report.conversion_error( + &format!("COSE protected header collaborator index {ids}"), + &format!("{collaborator:?}"), + &format!("Expected String: {e:?}"), + &format!("{CONTEXT}, converting collaborator to String"), + ); }, } } extra.collabs = c; }, Err(e) => { - errors.push(anyhow!( - "Invalid COSE protected header `collabs` field, err: {e:?}" - )); + error_report.conversion_error( + "CBOR COSE protected header collaborators", + &format!("{cbor_doc_collabs:?}"), + &format!("Expected Array: {e:?}"), + &format!("{CONTEXT}, converting collaborators to Array"), + ); }, } } @@ -263,9 +283,12 @@ impl TryFrom<&ProtectedHeader> for ExtraFields { extra.brand_id = Some(brand_id); }, Err(e) => { - errors.push(anyhow!( - "Invalid COSE protected header `brand_id` field, err: {e}" - )); + error_report.conversion_error( + "CBOR COSE protected header brand ID", + &format!("{cbor_doc_brand_id:?}"), + &format!("Expected UUID: {e:?}"), + &format!("{CONTEXT}, decoding CBOR UUID for brand ID"), + ); }, } } @@ -278,9 +301,12 @@ impl TryFrom<&ProtectedHeader> for ExtraFields { extra.campaign_id = Some(campaign_id); }, Err(e) => { - errors.push(anyhow!( - "Invalid COSE protected header `campaign_id` field, err: {e}" - )); + error_report.conversion_error( + "CBOR COSE protected header campaign ID", + &format!("{cbor_doc_campaign_id:?}"), + &format!("Expected UUID: {e:?}"), + &format!("{CONTEXT}, decoding CBOR UUID for campaign ID"), + ); }, } } @@ -293,9 +319,12 @@ impl TryFrom<&ProtectedHeader> for ExtraFields { extra.election_id = Some(election_id); }, Err(e) => { - errors.push(anyhow!( - "Invalid COSE protected header `election_id` field, err: {e}" - )); + error_report.conversion_error( + "CBOR COSE protected header election ID", + &format!("{cbor_doc_election_id:?}"), + &format!("Expected UUID: {e:?}"), + &format!("{CONTEXT}, decoding CBOR UUID for election ID"), + ); }, } } @@ -308,18 +337,20 @@ impl TryFrom<&ProtectedHeader> for ExtraFields { extra.category_id = Some(category_id); }, Err(e) => { - errors.push(anyhow!( - "Invalid COSE protected header `category_id` field, err: {e}" - )); + error_report.conversion_error( + "CBOR COSE protected header category ID", + &format!("{cbor_doc_category_id:?}"), + &format!("Expected UUID: {e:?}"), + &format!("{CONTEXT}, decoding CBOR UUID for category ID"), + ); }, } } - if errors.is_empty() { - Ok(extra) - } else { - Err(errors.into()) + if error_report.is_problematic() { + bail!("Failed to convert COSE ProtectedHeader to ExtraFields"); } + Ok(extra) } } diff --git a/rust/signed_doc/src/metadata/mod.rs b/rust/signed_doc/src/metadata/mod.rs index ac16bac05a..5722e99ba8 100644 --- a/rust/signed_doc/src/metadata/mod.rs +++ b/rust/signed_doc/src/metadata/mod.rs @@ -11,7 +11,8 @@ mod document_version; mod extra_fields; use algorithm::Algorithm; -use anyhow::anyhow; +use anyhow::{anyhow, bail}; +use catalyst_types::problem_report::ProblemReport; pub use catalyst_types::uuid::{CborContext, UuidV4, UuidV7}; pub use content_encoding::ContentEncoding; pub use content_type::ContentType; @@ -93,75 +94,50 @@ impl Metadata { pub fn extra(&self) -> &ExtraFields { &self.extra } -} - -impl Display for Metadata { - fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { - writeln!(f, "Metadata {{")?; - writeln!(f, " type: {},", self.doc_type)?; - writeln!(f, " id: {},", self.id)?; - writeln!(f, " ver: {},", self.ver)?; - writeln!(f, " alg: {:?},", self.alg)?; - writeln!(f, " content_type: {}", self.content_type)?; - writeln!(f, " content_encoding: {:?}", self.content_encoding)?; - writeln!(f, " additional_fields: {:?},", self.extra)?; - writeln!(f, "}}") - } -} - -impl TryFrom<&Metadata> for coset::Header { - type Error = anyhow::Error; - - fn try_from(meta: &Metadata) -> Result { - let mut builder = coset::HeaderBuilder::new() - .algorithm(meta.alg.into()) - .content_format(CoapContentFormat::from(meta.content_type())); - - if let Some(content_encoding) = meta.content_encoding() { - builder = builder.text_value( - CONTENT_ENCODING_KEY.to_string(), - format!("{content_encoding}").into(), - ); - } - - builder = builder - .text_value(TYPE_KEY.to_string(), meta.doc_type.try_into()?) - .text_value(ID_KEY.to_string(), meta.id.try_into()?) - .text_value(VER_KEY.to_string(), meta.ver.try_into()?); - - builder = meta.extra.fill_cose_header_fields(builder)?; - - Ok(builder.build()) - } -} - -impl TryFrom<&coset::ProtectedHeader> for Metadata { - type Error = crate::error::Error; + /// Converting COSE Protected Header to Metadata. #[allow(clippy::too_many_lines)] - fn try_from(protected: &coset::ProtectedHeader) -> Result { - let mut errors = Vec::new(); + pub(crate) fn from_protected_header( + protected: &coset::ProtectedHeader, error_report: &ProblemReport, + ) -> anyhow::Result { + /// Context for error messages. + const CONTEXT: &str = "COSE Protected Header to Metadata"; let mut algorithm = Algorithm::default(); if let Some(coset::RegisteredLabelWithPrivate::Assigned(alg)) = protected.header.alg { match Algorithm::try_from(alg) { Ok(alg) => algorithm = alg, - Err(e) => errors.push(anyhow!("Invalid Document Algorithm: {e}")), + Err(e) => { + error_report.conversion_error( + "COSE protected header algorithm", + &format!("{alg:?}"), + &format!("Expected Algorithm: {e}"), + &format!("{CONTEXT}, Algorithm"), + ); + }, } } else { - errors.push(anyhow!("Invalid COSE protected header, missing alg field")); + error_report.missing_field("alg", "Missing alg field in COSE protected header"); } let mut content_type = None; if let Some(value) = protected.header.content_type.as_ref() { match ContentType::try_from(value) { Ok(ct) => content_type = Some(ct), - Err(e) => errors.push(anyhow!("Invalid Document Content-Type: {e}")), + Err(e) => { + error_report.conversion_error( + "COSE protected header content type", + &format!("{value:?}"), + &format!("Expected ContentType: {e}"), + &format!("{CONTEXT}, ContentType"), + ); + }, } } else { - errors.push(anyhow!( - "Invalid COSE protected header, missing Content-Type field" - )); + error_report.missing_field( + "content type", + "Missing content_type field in COSE protected header", + ); } let mut content_encoding = None; @@ -171,12 +147,20 @@ impl TryFrom<&coset::ProtectedHeader> for Metadata { ) { match ContentEncoding::try_from(value) { Ok(ce) => content_encoding = Some(ce), - Err(e) => errors.push(anyhow!("Invalid Document Content Encoding: {e}")), + Err(e) => { + error_report.conversion_error( + "COSE protected header content encoding", + &format!("{value:?}"), + &format!("Expected ContentEncoding: {e}"), + &format!("{CONTEXT}, ContentEncoding"), + ); + }, } } else { - errors.push(anyhow!( - "Invalid COSE protected header, missing Content-Encoding field" - )); + error_report.missing_field( + "content encoding", + "Missing content encoding field in COSE protected header", + ); } let mut doc_type: Option = None; @@ -185,12 +169,17 @@ impl TryFrom<&coset::ProtectedHeader> for Metadata { }) { match decode_cbor_uuid(value.clone()) { Ok(uuid) => doc_type = Some(uuid), - Err(e) => errors.push(anyhow!("Invalid document type UUID: {e}")), + Err(e) => { + error_report.conversion_error( + "COSE protected header type", + &format!("{value:?}"), + &format!("Expected UUID: {e:?}"), + &format!("{CONTEXT}, decoding CBOR UUID for type"), + ); + }, } } else { - errors.push(anyhow!( - "Invalid COSE protected header, missing `type` field" - )); + error_report.missing_field("type", "Missing type field in COSE protected header"); } let mut id: Option = None; @@ -199,10 +188,17 @@ impl TryFrom<&coset::ProtectedHeader> for Metadata { }) { match decode_cbor_uuid(value.clone()) { Ok(uuid) => id = Some(uuid), - Err(e) => errors.push(anyhow!("Invalid document ID UUID: {e}")), + Err(e) => { + error_report.conversion_error( + "COSE protected header ID", + &format!("{value:?}"), + &format!("Expected UUID: {e:?}"), + &format!("{CONTEXT}, decoding CBOR UUID for ID"), + ); + }, } } else { - errors.push(anyhow!("Invalid COSE protected header, missing `id` field")); + error_report.missing_field("id", "Missing id field in COSE protected header"); } let mut ver: Option = None; @@ -211,17 +207,27 @@ impl TryFrom<&coset::ProtectedHeader> for Metadata { }) { match decode_cbor_uuid(value.clone()) { Ok(uuid) => ver = Some(uuid), - Err(e) => errors.push(anyhow!("Invalid document version UUID: {e}")), + Err(e) => { + error_report.conversion_error( + "COSE protected header ver", + &format!("{value:?}"), + &format!("Expected UUID: {e:?}"), + &format!("{CONTEXT}, decoding CBOR UUID for version"), + ); + }, } } else { - errors.push(anyhow!( - "Invalid COSE protected header, missing `ver` field" - )); + error_report.missing_field("ver", "Missing ver field in COSE protected header"); } - let extra = ExtraFields::try_from(protected).map_or_else( + let extra = ExtraFields::from_protected_header(protected, error_report).map_or_else( |e| { - errors.extend(e.0 .0); + error_report.conversion_error( + "COSE protected header", + &format!("{protected:?}"), + &format!("Expected ExtraField: {e}"), + &format!("{CONTEXT}, ExtraFields"), + ); None }, Some, @@ -237,10 +243,14 @@ impl TryFrom<&coset::ProtectedHeader> for Metadata { Some(extra), ) => { if ver < id { - errors.push(anyhow!( - "Document Version {ver} cannot be smaller than Document ID {id}", - )); - return Err(crate::error::Error(errors.into())); + error_report.invalid_value( + "ver", + &ver.to_string(), + "ver < id", + &format!("{CONTEXT}, Document Version {ver} cannot be smaller than Document ID {id}"), + ); + + bail!("Failed to convert COSE Protected Header to Metadata: document version is smaller than document ID"); } Ok(Self { @@ -253,8 +263,48 @@ impl TryFrom<&coset::ProtectedHeader> for Metadata { extra, }) }, - _ => Err(crate::error::Error(errors.into())), + _ => bail!("Failed to convert COSE Protected Header to Metadata"), + } + } +} + +impl Display for Metadata { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { + writeln!(f, "Metadata {{")?; + writeln!(f, " type: {},", self.doc_type)?; + writeln!(f, " id: {},", self.id)?; + writeln!(f, " ver: {},", self.ver)?; + writeln!(f, " alg: {:?},", self.alg)?; + writeln!(f, " content_type: {}", self.content_type)?; + writeln!(f, " content_encoding: {:?}", self.content_encoding)?; + writeln!(f, " additional_fields: {:?},", self.extra)?; + writeln!(f, "}}") + } +} + +impl TryFrom<&Metadata> for coset::Header { + type Error = anyhow::Error; + + fn try_from(meta: &Metadata) -> Result { + let mut builder = coset::HeaderBuilder::new() + .algorithm(meta.alg.into()) + .content_format(CoapContentFormat::from(meta.content_type())); + + if let Some(content_encoding) = meta.content_encoding() { + builder = builder.text_value( + CONTENT_ENCODING_KEY.to_string(), + format!("{content_encoding}").into(), + ); } + + builder = builder + .text_value(TYPE_KEY.to_string(), meta.doc_type.try_into()?) + .text_value(ID_KEY.to_string(), meta.id.try_into()?) + .text_value(VER_KEY.to_string(), meta.ver.try_into()?); + + builder = meta.extra.fill_cose_header_fields(builder)?; + + Ok(builder.build()) } } diff --git a/rust/signed_doc/src/signature/mod.rs b/rust/signed_doc/src/signature/mod.rs index 7fa07cf51d..3c1ff99c7d 100644 --- a/rust/signed_doc/src/signature/mod.rs +++ b/rust/signed_doc/src/signature/mod.rs @@ -1,6 +1,8 @@ //! Catalyst Signed Document COSE Signature information. +use anyhow::bail; pub use catalyst_types::kid_uri::KidUri; +use catalyst_types::problem_report::ProblemReport; use coset::CoseSignature; /// Catalyst Signed Document COSE Signature. @@ -46,15 +48,14 @@ impl Signatures { pub fn is_empty(&self) -> bool { self.0.is_empty() } -} - -impl TryFrom<&Vec> for Signatures { - type Error = crate::error::Error; - fn try_from(value: &Vec) -> Result { + /// Convert list of COSE Signature to `Signatures`. + pub(crate) fn from_cose_sig( + cose_sigs: &[CoseSignature], error_report: &ProblemReport, + ) -> anyhow::Result { let mut signatures = Vec::new(); - let mut errors = Vec::new(); - value + + cose_sigs .iter() .cloned() .enumerate() @@ -62,17 +63,18 @@ impl TryFrom<&Vec> for Signatures { match KidUri::try_from(signature.protected.header.key_id.as_ref()) { Ok(kid) => signatures.push(Signature { kid, signature }), Err(e) => { - errors.push(anyhow::anyhow!( - "Signature at index {idx} has valid Catalyst Key Id: {e}" - )); + error_report.conversion_error( + &format!("COSE signature protected header key ID at id {idx}"), + &format!("{:?}", &signature.protected.header.key_id), + &format!("{e:?}"), + "Converting COSE signature header key ID to KidUri", + ); }, } }); - - if errors.is_empty() { - Ok(Signatures(signatures)) - } else { - Err(errors.into()) + if error_report.is_problematic() { + bail!("Failed to convert COSE Signatures to Signatures"); } + Ok(Signatures(signatures)) } } diff --git a/rust/signed_doc/src/utils/context.rs b/rust/signed_doc/src/utils/context.rs new file mode 100644 index 0000000000..ef48f42777 --- /dev/null +++ b/rust/signed_doc/src/utils/context.rs @@ -0,0 +1,9 @@ +//! Contexts for the Signed Document. + +use catalyst_types::problem_report::ProblemReport; + +/// Sign Document Decoding Context. +pub(crate) struct DecodeSignDocCtx { + /// Error Report. + pub(crate) error_report: ProblemReport, +} diff --git a/rust/signed_doc/src/utils/mod.rs b/rust/signed_doc/src/utils/mod.rs new file mode 100644 index 0000000000..8cfdf34fdb --- /dev/null +++ b/rust/signed_doc/src/utils/mod.rs @@ -0,0 +1,3 @@ +//! Utility functions. + +pub(crate) mod context;