diff --git a/rust/catalyst-types/src/catalyst_id/mod.rs b/rust/catalyst-types/src/catalyst_id/mod.rs index 1612cec4a1..0a51516fa5 100644 --- a/rust/catalyst-types/src/catalyst_id/mod.rs +++ b/rust/catalyst-types/src/catalyst_id/mod.rs @@ -676,12 +676,9 @@ impl TryFrom<&[u8]> for CatalystId { } } -impl minicbor::Encode<()> for CatalystId { - fn encode( - &self, e: &mut minicbor::Encoder, _ctx: &mut (), - ) -> Result<(), minicbor::encode::Error> { - e.bytes(self.to_string().into_bytes().as_slice())?; - Ok(()) +impl From<&CatalystId> for Vec { + fn from(value: &CatalystId) -> Self { + value.to_string().into_bytes() } } diff --git a/rust/cbork-utils/src/deterministic_helper.rs b/rust/cbork-utils/src/deterministic_helper.rs index b2855c9e6a..7c68692f27 100644 --- a/rust/cbork-utils/src/deterministic_helper.rs +++ b/rust/cbork-utils/src/deterministic_helper.rs @@ -98,12 +98,11 @@ impl Ord for MapEntry { /// - Map keys are not properly sorted (`UnorderedMapKeys`) /// - Duplicate keys are found (`DuplicateMapKey`) /// - Map key or value decoding fails (`DecoderError`) -pub fn decode_map_deterministically(d: &mut Decoder) -> Result, minicbor::decode::Error> { +pub fn decode_map_deterministically( + d: &mut Decoder, +) -> Result, minicbor::decode::Error> { validate_input_not_empty(d)?; - // Store the starting position BEFORE consuming the map header - let map_start = d.position(); - // From RFC 8949 Section 4.2.2: // "Indefinite-length items must be made definite-length items." // The specification explicitly prohibits indefinite-length items in @@ -115,7 +114,6 @@ pub fn decode_map_deterministically(d: &mut Decoder) -> Result, minicbor })?; let header_end_pos = d.position(); - check_map_minimal_length(d, header_end_pos, map_len)?; // Decode entries to validate them @@ -123,10 +121,7 @@ pub fn decode_map_deterministically(d: &mut Decoder) -> Result, minicbor validate_map_ordering(&entries)?; - // Get the ending position after validation - let map_end = d.position(); - - get_bytes(d, map_start, map_end) + Ok(entries) } /// Extracts the raw bytes of a CBOR map from a decoder based on specified positions. @@ -501,7 +496,21 @@ mod tests { let result = decode_map_deterministically(&mut decoder).unwrap(); // Verify we got back exactly the same bytes - assert_eq!(result, valid_map); + + assert_eq!(result, vec![ + MapEntry { + // Key 1: 2-byte string + key_bytes: vec![0x42, 0x01, 0x02], + // Value 1: 1-byte string + value: vec![0x41, 0x01] + }, + MapEntry { + // Key 2: 3-byte string + key_bytes: vec![0x43, 0x01, 0x02, 0x03,], + // Value 2: 1-byte string + value: vec![0x41, 0x02,] + } + ]); } /// Test cases for lexicographic ordering of map keys as specified in RFC 8949 Section diff --git a/rust/signed_doc/Cargo.toml b/rust/signed_doc/Cargo.toml index 151de0a34f..1c1f75fe94 100644 --- a/rust/signed_doc/Cargo.toml +++ b/rust/signed_doc/Cargo.toml @@ -12,11 +12,11 @@ workspace = true [dependencies] catalyst-types = { version = "0.0.3", path = "../catalyst-types" } +cbork-utils = { version = "0.0.1", path = "../cbork-utils" } anyhow = "1.0.95" serde = { version = "1.0.217", features = ["derive"] } serde_json = { version = "1.0.134", features = ["raw_value"] } -coset = "0.3.8" minicbor = { version = "0.25.1", features = ["half"] } brotli = "7.0.0" ed25519-dalek = { version = "2.1.1", features = ["rand_core", "pem"] } diff --git a/rust/signed_doc/src/content.rs b/rust/signed_doc/src/content.rs index 30ed78174e..1126b0711f 100644 --- a/rust/signed_doc/src/content.rs +++ b/rust/signed_doc/src/content.rs @@ -36,3 +36,18 @@ impl minicbor::Encode<()> for Content { Ok(()) } } + +impl minicbor::Decode<'_, ()> for Content { + fn decode( + d: &mut minicbor::Decoder<'_>, _ctx: &mut (), + ) -> Result { + let p = d.position(); + d.null() + .map(|()| Self(Vec::new())) + // important to use `or_else` so it will lazy evaluated at the time when it is needed + .or_else(|_| { + d.set_position(p); + d.bytes().map(Vec::from).map(Self) + }) + } +} diff --git a/rust/signed_doc/src/lib.rs b/rust/signed_doc/src/lib.rs index f9b67abd58..3700f910a3 100644 --- a/rust/signed_doc/src/lib.rs +++ b/rust/signed_doc/src/lib.rs @@ -21,7 +21,6 @@ pub use catalyst_types::{ uuid::{Uuid, UuidV4, UuidV7}, }; pub use content::Content; -use coset::{CborSerializable, TaggedCborSerializable}; use decode_context::{CompatibilityPolicy, DecodeContext}; pub use metadata::{ ContentEncoding, ContentType, DocLocator, DocType, DocumentRef, DocumentRefs, Metadata, Section, @@ -31,8 +30,8 @@ pub use signature::{CatalystId, Signatures}; use crate::builder::SignaturesBuilder; -/// A problem report content string -const PROBLEM_REPORT_CTX: &str = "Catalyst Signed Document"; +/// `COSE_Sign` object CBOR tag +const COSE_SIGN_CBOR_TAG: minicbor::data::Tag = minicbor::data::Tag::new(98); /// Inner type that holds the Catalyst Signed Document with parsing errors. #[derive(Debug)] @@ -212,34 +211,51 @@ impl CatalystSignedDocument { impl Decode<'_, ()> for CatalystSignedDocument { fn decode(d: &mut Decoder<'_>, _ctx: &mut ()) -> Result { - let start = d.position(); - d.skip()?; - let end = d.position(); - let cose_bytes = d - .input() - .get(start..end) - .ok_or(minicbor::decode::Error::end_of_input())?; - - let cose_sign = coset::CoseSign::from_tagged_slice(cose_bytes) - .or_else(|_| coset::CoseSign::from_slice(cose_bytes)) - .map_err(|e| { - minicbor::decode::Error::message(format!("Invalid COSE Sign document: {e}")) - })?; - - let mut report = ProblemReport::new(PROBLEM_REPORT_CTX); + let mut report = ProblemReport::new("Catalyst Signed Document Decoding"); let mut ctx = DecodeContext { compatibility_policy: CompatibilityPolicy::Accept, report: &mut report, }; - let metadata = Metadata::from_protected_header(&cose_sign.protected, &mut ctx); - let signatures = Signatures::from_cose_sig_list(&cose_sign.signatures, &report); + let start = d.position(); - let content = if let Some(payload) = cose_sign.payload { - payload.into() + if let Ok(tag) = d.tag() { + if tag != COSE_SIGN_CBOR_TAG { + return Err(minicbor::decode::Error::message(format!( + "Must be equal to the COSE_Sign tag value: {COSE_SIGN_CBOR_TAG}" + ))); + } } else { - report.missing_field("COSE Sign Payload", "Missing document content (payload)"); - Content::default() - }; + d.set_position(start); + } + + if !matches!(d.array()?, Some(4)) { + return Err(minicbor::decode::Error::message( + "Must be a definite size array of 4 elements", + )); + } + + let metadata_bytes = d.bytes()?; + let metadata = Metadata::decode(&mut minicbor::Decoder::new(metadata_bytes), &mut ctx)?; + + // empty unprotected headers + let mut map = + cbork_utils::deterministic_helper::decode_map_deterministically(d)?.into_iter(); + if map.next().is_some() { + ctx.report.unknown_field( + "unprotected headers", + "non empty unprotected headers", + "COSE unprotected headers must be empty", + ); + } + + let content = Content::decode(d, &mut ())?; + let signatures = Signatures::decode(d, &mut ctx)?; + + let end = d.position(); + let cose_bytes = d + .input() + .get(start..end) + .ok_or(minicbor::decode::Error::end_of_input())?; Ok(InnerCatalystSignedDocument { metadata, diff --git a/rust/signed_doc/src/metadata/collaborators.rs b/rust/signed_doc/src/metadata/collaborators.rs new file mode 100644 index 0000000000..d5819cc046 --- /dev/null +++ b/rust/signed_doc/src/metadata/collaborators.rs @@ -0,0 +1,57 @@ +//! Catalyst Signed Document `collabs` field type definition. + +use std::ops::Deref; + +/// 'collabs' field type definition, which is a JSON path string +#[derive(Clone, Debug, PartialEq)] +pub struct Collaborators(Vec); + +impl Deref for Collaborators { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl minicbor::Encode<()> for Collaborators { + fn encode( + &self, e: &mut minicbor::Encoder, _ctx: &mut (), + ) -> Result<(), minicbor::encode::Error> { + if !self.0.is_empty() { + e.array( + self.0 + .len() + .try_into() + .map_err(minicbor::encode::Error::message)?, + )?; + for c in &self.0 { + e.str(c)?; + } + } + Ok(()) + } +} + +impl minicbor::Decode<'_, ()> for Collaborators { + fn decode( + d: &mut minicbor::Decoder<'_>, _ctx: &mut (), + ) -> Result { + let Some(items) = d.array()? else { + return Err(minicbor::decode::Error::message( + "Must a definite size array", + )); + }; + let collabs = (0..items) + .map(|_| Ok(d.str()?.to_string())) + .collect::>()?; + Ok(Self(collabs)) + } +} + +impl<'de> serde::Deserialize<'de> for Collaborators { + fn deserialize(deserializer: D) -> Result + where D: serde::Deserializer<'de> { + Ok(Self(Vec::::deserialize(deserializer)?)) + } +} diff --git a/rust/signed_doc/src/metadata/content_encoding.rs b/rust/signed_doc/src/metadata/content_encoding.rs index e736dc74d6..3a9ff1ad76 100644 --- a/rust/signed_doc/src/metadata/content_encoding.rs +++ b/rust/signed_doc/src/metadata/content_encoding.rs @@ -72,19 +72,6 @@ impl<'de> Deserialize<'de> for ContentEncoding { } } -impl TryFrom<&coset::cbor::Value> for ContentEncoding { - type Error = anyhow::Error; - - fn try_from(val: &coset::cbor::Value) -> anyhow::Result { - match val.as_text() { - Some(encoding) => encoding.parse(), - None => { - anyhow::bail!("Expected Content Encoding to be a string"); - }, - } - } -} - impl minicbor::Encode<()> for ContentEncoding { fn encode( &self, e: &mut minicbor::Encoder, _ctx: &mut (), @@ -93,3 +80,11 @@ impl minicbor::Encode<()> for ContentEncoding { Ok(()) } } + +impl minicbor::Decode<'_, ()> for ContentEncoding { + fn decode( + d: &mut minicbor::Decoder<'_>, _ctx: &mut (), + ) -> Result { + d.str()?.parse().map_err(minicbor::decode::Error::message) + } +} diff --git a/rust/signed_doc/src/metadata/content_type.rs b/rust/signed_doc/src/metadata/content_type.rs index 260c0196b5..d22dcad2da 100644 --- a/rust/signed_doc/src/metadata/content_type.rs +++ b/rust/signed_doc/src/metadata/content_type.rs @@ -5,7 +5,6 @@ use std::{ str::FromStr, }; -use coset::iana::CoapContentFormat; use serde::{de, Deserialize, Deserializer}; use strum::VariantArray; @@ -55,27 +54,6 @@ impl<'de> Deserialize<'de> for ContentType { } } -impl TryFrom<&coset::ContentType> for ContentType { - type Error = anyhow::Error; - - fn try_from(value: &coset::ContentType) -> Result { - match value { - coset::ContentType::Assigned(CoapContentFormat::Json) => Ok(ContentType::Json), - coset::ContentType::Assigned(CoapContentFormat::Cbor) => Ok(ContentType::Cbor), - coset::ContentType::Text(str) => str.parse(), - coset::RegisteredLabel::Assigned(_) => { - anyhow::bail!( - "Unsupported Content Type: {value:?}, Supported only: {:?}", - ContentType::VARIANTS - .iter() - .map(ToString::to_string) - .collect::>() - ) - }, - } - } -} - impl minicbor::Encode<()> for ContentType { fn encode( &self, e: &mut minicbor::Encoder, _ctx: &mut (), @@ -86,6 +64,29 @@ impl minicbor::Encode<()> for ContentType { } } +impl minicbor::Decode<'_, ()> for ContentType { + fn decode( + d: &mut minicbor::Decoder<'_>, _ctx: &mut (), + ) -> Result { + let p = d.position(); + match d.int() { + // CoAP Content Format JSON + Ok(val) if val == minicbor::data::Int::from(50_u8) => Ok(Self::Json), + // CoAP Content Format CBOR + Ok(val) if val == minicbor::data::Int::from(60_u8) => Ok(Self::Cbor), + Ok(val) => { + Err(minicbor::decode::Error::message(format!( + "unsupported CoAP Content Formats value: {val}" + ))) + }, + Err(_) => { + d.set_position(p); + d.str()?.parse().map_err(minicbor::decode::Error::message) + }, + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/rust/signed_doc/src/metadata/doc_type.rs b/rust/signed_doc/src/metadata/doc_type.rs index c55a45be5e..adf09faa64 100644 --- a/rust/signed_doc/src/metadata/doc_type.rs +++ b/rust/signed_doc/src/metadata/doc_type.rs @@ -5,8 +5,7 @@ use std::{ hash::{Hash, Hasher}, }; -use catalyst_types::uuid::{CborContext, Uuid, UuidV4, UUID_CBOR_TAG}; -use coset::cbor::Value; +use catalyst_types::uuid::{CborContext, Uuid, UuidV4}; use minicbor::{Decode, Decoder, Encode}; use serde::{Deserialize, Deserializer}; use tracing::warn; @@ -296,23 +295,6 @@ impl<'de> Deserialize<'de> for DocType { } } -impl From for Value { - fn from(value: DocType) -> Self { - Value::Array( - value - .0 - .iter() - .map(|uuidv4| { - Value::Tag( - UUID_CBOR_TAG, - Box::new(Value::Bytes(uuidv4.uuid().as_bytes().to_vec())), - ) - }) - .collect(), - ) - } -} - // This is needed to preserve backward compatibility with the old solution. impl PartialEq for DocType { fn eq(&self, other: &Self) -> bool { @@ -448,18 +430,6 @@ mod tests { assert!(matches!(result, Err(DocTypeError::StringConversion(s)) if s == "not-a-uuid")); } - #[test] - fn test_doc_type_to_value() { - let uuid = uuid::Uuid::new_v4(); - let doc_type: Value = DocType(vec![UuidV4::try_from(uuid).unwrap()]).into(); - - for d in &doc_type.into_array().unwrap() { - let t = d.clone().into_tag().unwrap(); - assert_eq!(t.0, UUID_CBOR_TAG); - assert_eq!(t.1.as_bytes().unwrap().len(), 16); - } - } - #[test] fn test_doctype_equal_special_cases() { // Direct equal diff --git a/rust/signed_doc/src/metadata/document_refs/doc_locator.rs b/rust/signed_doc/src/metadata/document_refs/doc_locator.rs index 61e6bac229..303f4624d2 100644 --- a/rust/signed_doc/src/metadata/document_refs/doc_locator.rs +++ b/rust/signed_doc/src/metadata/document_refs/doc_locator.rs @@ -5,7 +5,6 @@ use std::fmt::Display; use catalyst_types::problem_report::ProblemReport; -use coset::cbor::Value; use minicbor::{Decode, Decoder, Encode}; /// CBOR tag of IPLD content identifiers (CIDs). @@ -47,15 +46,6 @@ impl Display for DocLocator { } } -impl From for Value { - fn from(value: DocLocator) -> Self { - Value::Map(vec![( - Value::Text(CID_MAP_KEY.to_string()), - Value::Tag(CID_TAG, Box::new(Value::Bytes(value.0.clone()))), - )]) - } -} - // document_locator = { "cid" => cid } impl Decode<'_, ProblemReport> for DocLocator { fn decode( @@ -157,19 +147,4 @@ mod tests { let decoded_doc_loc = DocLocator::decode(&mut decoder, &mut report).unwrap(); assert_eq!(locator, decoded_doc_loc); } - - #[test] - #[allow(clippy::indexing_slicing)] - fn test_doc_locator_to_value() { - let data = vec![1, 2, 3, 4]; - let locator = DocLocator(data.clone()); - let value: Value = locator.into(); - let map = value.into_map().unwrap(); - assert_eq!(map.len(), usize::try_from(DOC_LOC_MAP_ITEM).unwrap()); - let key = map[0].0.clone().into_text().unwrap(); - assert_eq!(key, CID_MAP_KEY); - let (tag, value) = map[0].1.clone().into_tag().unwrap(); - assert_eq!(tag, CID_TAG); - assert_eq!(value.into_bytes().unwrap(), data); - } } diff --git a/rust/signed_doc/src/metadata/document_refs/doc_ref.rs b/rust/signed_doc/src/metadata/document_refs/doc_ref.rs index 2339fc6450..0923e9414a 100644 --- a/rust/signed_doc/src/metadata/document_refs/doc_ref.rs +++ b/rust/signed_doc/src/metadata/document_refs/doc_ref.rs @@ -3,11 +3,10 @@ use std::fmt::Display; use catalyst_types::uuid::{CborContext, UuidV7}; -use coset::cbor::Value; use minicbor::{Decode, Decoder, Encode}; -use super::{doc_locator::DocLocator, DocRefError}; -use crate::{metadata::utils::CborUuidV7, DecodeContext}; +use super::doc_locator::DocLocator; +use crate::DecodeContext; /// Number of item that should be in each document reference instance. const DOC_REF_ARR_ITEM: u64 = 3; @@ -63,22 +62,6 @@ impl Display for DocumentRef { } } -impl TryFrom for Value { - type Error = DocRefError; - - fn try_from(value: DocumentRef) -> Result { - let id = Value::try_from(CborUuidV7(value.id)) - .map_err(|_| DocRefError::InvalidUuidV7(value.id, "id".to_string()))?; - - let ver = Value::try_from(CborUuidV7(value.ver)) - .map_err(|_| DocRefError::InvalidUuidV7(value.ver, "ver".to_string()))?; - - let locator = value.doc_locator.clone().into(); - - Ok(Value::Array(vec![id, ver, locator])) - } -} - impl Decode<'_, DecodeContext<'_>> for DocumentRef { fn decode( d: &mut minicbor::Decoder<'_>, decode_context: &mut DecodeContext<'_>, @@ -143,29 +126,3 @@ impl Encode<()> for DocumentRef { Ok(()) } } - -#[cfg(test)] -mod test { - use catalyst_types::uuid::{UuidV7, UUID_CBOR_TAG}; - use coset::cbor::Value; - - use crate::metadata::document_refs::{doc_ref::DOC_REF_ARR_ITEM, DocumentRef}; - - #[test] - #[allow(clippy::indexing_slicing)] - fn test_doc_refs_to_value() { - let uuidv7 = UuidV7::new(); - let doc_ref = DocumentRef::new(uuidv7, uuidv7, vec![1, 2, 3].into()); - let value: Value = doc_ref.try_into().unwrap(); - let arr = value.into_array().unwrap(); - assert_eq!(arr.len(), usize::try_from(DOC_REF_ARR_ITEM).unwrap()); - let (id_tag, value) = arr[0].clone().into_tag().unwrap(); - assert_eq!(id_tag, UUID_CBOR_TAG); - assert_eq!(value.as_bytes().unwrap().len(), 16); - let (ver_tag, value) = arr[1].clone().into_tag().unwrap(); - assert_eq!(ver_tag, UUID_CBOR_TAG); - assert_eq!(value.as_bytes().unwrap().len(), 16); - let map = arr[2].clone().into_map().unwrap(); - assert_eq!(map.len(), 1); - } -} diff --git a/rust/signed_doc/src/metadata/document_refs/mod.rs b/rust/signed_doc/src/metadata/document_refs/mod.rs index 3c3cf6704f..022dc1c3c2 100644 --- a/rust/signed_doc/src/metadata/document_refs/mod.rs +++ b/rust/signed_doc/src/metadata/document_refs/mod.rs @@ -5,7 +5,6 @@ mod doc_ref; use std::{fmt::Display, str::FromStr}; use catalyst_types::uuid::{CborContext, UuidV7}; -use coset::cbor::Value; pub use doc_locator::DocLocator; pub use doc_ref::DocumentRef; use minicbor::{Decode, Decoder, Encode}; @@ -21,12 +20,6 @@ pub struct DocumentRefs(Vec); /// Document reference error. #[derive(Debug, Clone, thiserror::Error)] pub enum DocRefError { - /// Invalid `UUIDv7`. - #[error("Invalid UUID: {0} for field {1}")] - InvalidUuidV7(UuidV7, String), - /// `DocRef` cannot be empty. - #[error("DocType cannot be empty")] - Empty, /// Invalid string conversion #[error("Invalid string conversion: {0}")] StringConversion(String), @@ -168,32 +161,6 @@ impl Encode<()> for DocumentRefs { } } -impl TryFrom for Value { - type Error = DocRefError; - - fn try_from(value: DocumentRefs) -> Result { - if value.0.is_empty() { - return Err(DocRefError::Empty); - } - - let array_values: Result, Self::Error> = value - .0 - .iter() - .map(|inner| Value::try_from(inner.to_owned())) - .collect(); - - Ok(Value::Array(array_values?)) - } -} - -impl TryFrom<&DocumentRefs> for Value { - type Error = DocRefError; - - fn try_from(value: &DocumentRefs) -> Result { - value.clone().try_into() - } -} - impl<'de> Deserialize<'de> for DocumentRefs { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de> { @@ -360,15 +327,6 @@ mod tests { assert_eq!(decoded_doc_refs, doc_refs); } - #[test] - fn test_doc_refs_to_value() { - let uuidv7 = UuidV7::new(); - let doc_ref = DocumentRef::new(uuidv7, uuidv7, vec![1, 2, 3].into()); - let doc_ref = DocumentRefs(vec![doc_ref.clone(), doc_ref]); - let value: Value = doc_ref.try_into().unwrap(); - assert_eq!(value.as_array().unwrap().len(), 2); - } - #[test] fn test_deserialize_old_doc_ref() { let uuidv7 = UuidV7::new(); diff --git a/rust/signed_doc/src/metadata/mod.rs b/rust/signed_doc/src/metadata/mod.rs index 878e9e72aa..5b9f71f503 100644 --- a/rust/signed_doc/src/metadata/mod.rs +++ b/rust/signed_doc/src/metadata/mod.rs @@ -1,17 +1,16 @@ //! Catalyst Signed Document Metadata. use std::{ - collections::{btree_map, BTreeMap}, - error::Error, + collections::BTreeMap, fmt::{Display, Formatter}, }; +mod collaborators; mod content_encoding; mod content_type; pub(crate) mod doc_type; mod document_refs; mod section; mod supported_field; -pub(crate) mod utils; use catalyst_types::{problem_report::ProblemReport, uuid::UuidV7}; pub use content_encoding::ContentEncoding; @@ -21,38 +20,8 @@ pub use document_refs::{DocLocator, DocumentRef, DocumentRefs}; use minicbor::Decoder; pub use section::Section; use strum::IntoDiscriminant as _; -use utils::{cose_protected_header_find, decode_document_field_from_protected_header, CborUuidV7}; pub(crate) use crate::metadata::supported_field::{SupportedField, SupportedLabel}; -use crate::{decode_context::DecodeContext, metadata::utils::decode_cose_protected_header_value}; - -/// `content_encoding` field COSE key value -const CONTENT_ENCODING_KEY: &str = "Content-Encoding"; -/// `doc_type` field COSE key value -const TYPE_KEY: &str = "type"; -/// `id` field COSE key value -const ID_KEY: &str = "id"; -/// `ver` field COSE key value -const VER_KEY: &str = "ver"; - -/// `ref` field COSE key value -const REF_KEY: &str = "ref"; -/// `template` field COSE key value -const TEMPLATE_KEY: &str = "template"; -/// `reply` field COSE key value -const REPLY_KEY: &str = "reply"; -/// `section` field COSE key value -const SECTION_KEY: &str = "section"; -/// `collabs` field COSE key value -const COLLABS_KEY: &str = "collabs"; -/// `parameters` field COSE key value -const PARAMETERS_KEY: &str = "parameters"; -/// `brand_id` field COSE key value (alias of the `parameters` field) -const BRAND_ID_KEY: &str = "brand_id"; -/// `campaign_id` field COSE key value (alias of the `parameters` field) -const CAMPAIGN_ID_KEY: &str = "campaign_id"; -/// `category_id` field COSE key value (alias of the `parameters` field) -const CATEGORY_ID_KEY: &str = "category_id"; /// Document Metadata. /// @@ -155,7 +124,7 @@ impl Metadata { self.0 .get(&SupportedLabel::Collabs) .and_then(SupportedField::try_as_collabs_ref) - .map_or(&[], Vec::as_slice) + .map_or(&[], |v| &**v) } /// Return `parameters` field. @@ -179,11 +148,14 @@ impl Metadata { } /// Build `Metadata` object from the metadata fields, doing all necessary validation. - pub(crate) fn from_fields(fields: Vec, report: &ProblemReport) -> Self { + pub(crate) fn from_fields( + fields: impl Iterator>, report: &ProblemReport, + ) -> Result { const REPORT_CONTEXT: &str = "Metadata building"; let mut metadata = Metadata(BTreeMap::new()); for v in fields { + let v = v?; let k = v.discriminant(); if metadata.0.insert(k, v).is_some() { report.duplicate_field( @@ -207,183 +179,19 @@ impl Metadata { report.missing_field("content-type", REPORT_CONTEXT); } - metadata + Ok(metadata) } /// Build `Metadata` object from the metadata fields, doing all necessary validation. pub(crate) fn from_json(fields: serde_json::Value) -> anyhow::Result { let fields = serde::Deserializer::deserialize_map(fields, MetadataDeserializeVisitor)?; let report = ProblemReport::new("Deserializing metadata from json"); - let metadata = Self::from_fields(fields, &report); + let metadata = Self::from_fields(fields.into_iter().map(anyhow::Result::<_>::Ok), &report)?; anyhow::ensure!(!report.is_problematic(), "{:?}", report); Ok(metadata) } } -impl Metadata { - /// Converting COSE Protected Header to Metadata fields, collecting decoding report - /// issues. - #[allow( - clippy::too_many_lines, - reason = "This is a compilation of `coset` decoding and should be replaced once migrated to `minicbor`." - )] - pub(crate) fn from_protected_header( - protected: &coset::ProtectedHeader, context: &mut DecodeContext, - ) -> Self { - /// Context for problem report messages during decoding from COSE protected - /// header. - const COSE_DECODING_CONTEXT: &str = "COSE Protected Header to Metadata"; - - let mut metadata_fields = vec![]; - - if let Some(value) = protected.header.content_type.as_ref() { - match ContentType::try_from(value) { - Ok(ct) => metadata_fields.push(SupportedField::ContentType(ct)), - Err(e) => { - context.report.conversion_error( - "COSE protected header content type", - &format!("{value:?}"), - &format!("Expected ContentType: {e}"), - &format!("{COSE_DECODING_CONTEXT}, ContentType"), - ); - }, - } - } - - if let Some(value) = cose_protected_header_find( - protected, - |key| matches!(key, coset::Label::Text(label) if label.eq_ignore_ascii_case(CONTENT_ENCODING_KEY)), - ) { - match ContentEncoding::try_from(value) { - Ok(ce) => metadata_fields.push(SupportedField::ContentEncoding(ce)), - Err(e) => { - context.report.conversion_error( - "COSE protected header content encoding", - &format!("{value:?}"), - &format!("Expected ContentEncoding: {e}"), - &format!("{COSE_DECODING_CONTEXT}, ContentEncoding"), - ); - }, - } - } - - if let Some(value) = decode_document_field_from_protected_header::( - protected, - ID_KEY, - COSE_DECODING_CONTEXT, - context.report, - ) - .map(|v| v.0) - { - metadata_fields.push(SupportedField::Id(value)); - } - - if let Some(value) = decode_document_field_from_protected_header::( - protected, - VER_KEY, - COSE_DECODING_CONTEXT, - context.report, - ) - .map(|v| v.0) - { - metadata_fields.push(SupportedField::Ver(value)); - } - - // DocType and DocRef now using minicbor decoding. - if let Some(value) = decode_cose_protected_header_value::( - protected, context, TYPE_KEY, - ) { - metadata_fields.push(SupportedField::Type(value)); - }; - if let Some(value) = decode_cose_protected_header_value::( - protected, context, REF_KEY, - ) { - metadata_fields.push(SupportedField::Ref(value)); - }; - if let Some(value) = decode_cose_protected_header_value::( - protected, - context, - TEMPLATE_KEY, - ) { - metadata_fields.push(SupportedField::Template(value)); - } - if let Some(value) = decode_cose_protected_header_value::( - protected, context, REPLY_KEY, - ) { - metadata_fields.push(SupportedField::Reply(value)); - } - - if let Some(value) = decode_document_field_from_protected_header( - protected, - SECTION_KEY, - COSE_DECODING_CONTEXT, - context.report, - ) { - metadata_fields.push(SupportedField::Section(value)); - } - - // process `parameters` field and all its aliases - let (parameters, has_multiple_fields) = [ - PARAMETERS_KEY, - BRAND_ID_KEY, - CAMPAIGN_ID_KEY, - CATEGORY_ID_KEY, - ] - .iter() - .filter_map(|field_name| -> Option { - decode_cose_protected_header_value(protected, context, field_name) - }) - .fold((None, false), |(res, _), v| (Some(v), res.is_some())); - if has_multiple_fields { - context.report.duplicate_field( - "Parameters field", - "Only one parameter can be used at a time: either brand_id, campaign_id, category_id", - COSE_DECODING_CONTEXT - ); - } - if let Some(value) = parameters { - metadata_fields.push(SupportedField::Parameters(value)); - } - - if let Some(cbor_doc_collabs) = cose_protected_header_find(protected, |key| { - key == &coset::Label::Text(COLLABS_KEY.to_string()) - }) { - if let Ok(collabs) = cbor_doc_collabs.clone().into_array() { - let mut c = Vec::new(); - for (ids, collaborator) in collabs.iter().cloned().enumerate() { - match collaborator.clone().into_text() { - Ok(collaborator) => { - c.push(collaborator); - }, - Err(_) => { - context.report.conversion_error( - &format!("COSE protected header collaborator index {ids}"), - &format!("{collaborator:?}"), - "Expected a CBOR String", - &format!( - "{COSE_DECODING_CONTEXT}, converting collaborator to String", - ), - ); - }, - } - } - if !c.is_empty() { - metadata_fields.push(SupportedField::Collabs(c)); - } - } else { - context.report.conversion_error( - "CBOR COSE protected header collaborators", - &format!("{cbor_doc_collabs:?}"), - "Expected a CBOR Array", - &format!("{COSE_DECODING_CONTEXT}, converting collaborators to Array",), - ); - }; - } - - Self::from_fields(metadata_fields, context.report) - } -} - impl Display for Metadata { fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { writeln!(f, "Metadata {{")?; @@ -430,27 +238,6 @@ impl minicbor::Encode<()> for Metadata { } } -/// An error that's been reported, but doesn't affect the further decoding. -/// [`minicbor::Decoder`] should be assumed to be in a correct state and advanced towards -/// the next item. -/// -/// The wrapped error can be returned up the call stack. -#[derive(Debug, thiserror::Error)] -#[error(transparent)] -pub struct TransientDecodeError(pub minicbor::decode::Error); - -/// Creates a [`TransientDecodeError`] and wraps it in a -/// [`minicbor::decode::Error::custom`]. -fn custom_transient_decode_error( - message: &str, position: Option, -) -> minicbor::decode::Error { - let mut inner = minicbor::decode::Error::message(message); - if let Some(pos) = position { - inner = inner.at(pos); - } - minicbor::decode::Error::custom(TransientDecodeError(inner)) -} - impl minicbor::Decode<'_, crate::decode_context::DecodeContext<'_>> for Metadata { /// Decode from a CBOR map. /// @@ -464,53 +251,21 @@ impl minicbor::Decode<'_, crate::decode_context::DecodeContext<'_>> for Metadata fn decode( d: &mut Decoder<'_>, ctx: &mut crate::decode_context::DecodeContext<'_>, ) -> Result { - const REPORT_CONTEXT: &str = "Metadata decoding"; + // TODO: use helpers from `cbork-utils` crate to verify that's the map is + // deterministically CBOR encoded map. - let Some(len) = d.map()? else { + let Some(length) = d.map()? else { return Err(minicbor::decode::Error::message( - "Indefinite map is not supported", + "COSE protected headers object must be a definite size map ", )); }; - // TODO: verify key order. - // TODO: use helpers from once it's merged. - - let mut metadata_map = BTreeMap::new(); - let mut first_err = None; - - // This will return an error on the end of input. - for _ in 0..len { - let entry_pos = d.position(); - match d.decode_with::<_, SupportedField>(ctx) { - Ok(field) => { - let label = field.discriminant(); - let entry = metadata_map.entry(label); - if let btree_map::Entry::Vacant(entry) = entry { - entry.insert(field); - } else { - ctx.report.duplicate_field( - &label.to_string(), - "Duplicate metadata fields are not allowed", - REPORT_CONTEXT, - ); - first_err.get_or_insert(custom_transient_decode_error( - "Duplicate fields", - Some(entry_pos), - )); - } - }, - Err(err) - if err - .source() - .is_some_and(::is::) => - { - first_err.get_or_insert(err); - }, - Err(err) => return Err(err), - } - } + let report = ctx.report.clone(); + let fields = (0..length) + .map(|_| Option::::decode(d, ctx)) + .filter_map(Result::transpose); - first_err.map_or(Ok(Self(metadata_map)), Err) + Self::from_fields(fields, &report) } } diff --git a/rust/signed_doc/src/metadata/section.rs b/rust/signed_doc/src/metadata/section.rs index f4d4834415..cbe97fee3d 100644 --- a/rust/signed_doc/src/metadata/section.rs +++ b/rust/signed_doc/src/metadata/section.rs @@ -2,7 +2,6 @@ use std::{fmt::Display, str::FromStr}; -use coset::cbor::Value; use serde::{Deserialize, Serialize}; /// 'section' field type definition, which is a JSON path string @@ -40,17 +39,6 @@ impl FromStr for Section { } } -impl TryFrom<&Value> for Section { - type Error = anyhow::Error; - - fn try_from(val: &Value) -> anyhow::Result { - let str = val - .as_text() - .ok_or(anyhow::anyhow!("Not a cbor string type"))?; - Self::from_str(str) - } -} - impl minicbor::Encode<()> for Section { fn encode( &self, e: &mut minicbor::Encoder, _ctx: &mut (), @@ -59,3 +47,11 @@ impl minicbor::Encode<()> for Section { Ok(()) } } + +impl minicbor::Decode<'_, ()> for Section { + fn decode( + d: &mut minicbor::Decoder<'_>, _ctx: &mut (), + ) -> Result { + d.str()?.parse().map_err(minicbor::decode::Error::message) + } +} diff --git a/rust/signed_doc/src/metadata/supported_field.rs b/rust/signed_doc/src/metadata/supported_field.rs index 99572c4d7d..46fb58c802 100644 --- a/rust/signed_doc/src/metadata/supported_field.rs +++ b/rust/signed_doc/src/metadata/supported_field.rs @@ -9,7 +9,7 @@ use serde::Deserialize; use strum::{EnumDiscriminants, EnumTryAs, IntoDiscriminant as _}; use crate::{ - metadata::custom_transient_decode_error, ContentEncoding, ContentType, DocType, DocumentRefs, + metadata::collaborators::Collaborators, ContentEncoding, ContentType, DocType, DocumentRefs, Section, }; @@ -89,7 +89,7 @@ impl Display for Label<'_> { #[derive(Clone, Debug, PartialEq, EnumDiscriminants, EnumTryAs)] #[strum_discriminants( name(SupportedLabel), - derive(Ord, PartialOrd, serde::Deserialize), + derive(Ord, PartialOrd, serde::Deserialize, Hash), serde(rename_all = "kebab-case"), cfg_attr(test, derive(strum::VariantArray)) )] @@ -112,7 +112,7 @@ pub(crate) enum SupportedField { /// `reply` field. Reply(DocumentRefs) = 5, /// `collabs` field. - Collabs(Vec) = 7, + Collabs(Collaborators) = 7, /// `section` field. Section(Section) = 8, /// `template` field. @@ -197,14 +197,12 @@ impl<'de> serde::de::DeserializeSeed<'de> for SupportedLabel { } } -impl minicbor::Decode<'_, crate::decode_context::DecodeContext<'_>> for SupportedField { - #[allow(clippy::todo, reason = "Not migrated to `minicbor` yet.")] +impl minicbor::Decode<'_, crate::decode_context::DecodeContext<'_>> for Option { fn decode( d: &mut minicbor::Decoder<'_>, ctx: &mut crate::decode_context::DecodeContext<'_>, ) -> Result { const REPORT_CONTEXT: &str = "Metadata field decoding"; - let label_pos = d.position(); let label = Label::decode(d, &mut ())?; let Some(key) = SupportedLabel::from_cose(label) else { let value_start = d.position(); @@ -218,31 +216,40 @@ impl minicbor::Decode<'_, crate::decode_context::DecodeContext<'_>> for Supporte .to_string(); ctx.report .unknown_field(&label.to_string(), &value, REPORT_CONTEXT); - return Err(custom_transient_decode_error( - "Not a supported key", - Some(label_pos), - )); + return Ok(None); }; + let cbor_bytes = cbork_utils::decode_helper::decode_any(d, REPORT_CONTEXT)?; + let mut d = minicbor::Decoder::new(cbor_bytes); + let field = match key { - SupportedLabel::ContentType => todo!(), + SupportedLabel::ContentType => d.decode().map(SupportedField::ContentType), SupportedLabel::Id => { d.decode_with(&mut catalyst_types::uuid::CborContext::Tagged) - .map(Self::Id) + .map(SupportedField::Id) }, - SupportedLabel::Ref => d.decode_with(ctx).map(Self::Ref), + SupportedLabel::Ref => d.decode_with(ctx).map(SupportedField::Ref), SupportedLabel::Ver => { d.decode_with(&mut catalyst_types::uuid::CborContext::Tagged) - .map(Self::Ver) + .map(SupportedField::Ver) }, - SupportedLabel::Type => d.decode_with(ctx).map(Self::Type), - SupportedLabel::Reply => d.decode_with(ctx).map(Self::Reply), - SupportedLabel::Collabs => todo!(), - SupportedLabel::Section => todo!(), - SupportedLabel::Template => d.decode_with(ctx).map(Self::Template), - SupportedLabel::Parameters => d.decode_with(ctx).map(Self::Parameters), - SupportedLabel::ContentEncoding => todo!(), - }?; + SupportedLabel::Type => d.decode_with(ctx).map(SupportedField::Type), + SupportedLabel::Reply => d.decode_with(ctx).map(SupportedField::Reply), + SupportedLabel::Collabs => d.decode().map(SupportedField::Collabs), + SupportedLabel::Section => d.decode().map(SupportedField::Section), + SupportedLabel::Template => d.decode_with(ctx).map(SupportedField::Template), + SupportedLabel::Parameters => d.decode_with(ctx).map(SupportedField::Parameters), + SupportedLabel::ContentEncoding => d.decode().map(SupportedField::ContentEncoding), + } + .inspect_err(|e| { + ctx.report.invalid_value( + &format!("CBOR COSE protected header {key}"), + &hex::encode(cbor_bytes), + &format!("{e}"), + REPORT_CONTEXT, + ); + }) + .ok(); Ok(field) } @@ -265,20 +272,7 @@ impl minicbor::Encode<()> for SupportedField { | SupportedField::Template(document_ref) | SupportedField::Parameters(document_ref) => document_ref.encode(e, ctx), SupportedField::Type(doc_type) => doc_type.encode(e, ctx), - SupportedField::Collabs(collabs) => { - if !collabs.is_empty() { - e.array( - collabs - .len() - .try_into() - .map_err(minicbor::encode::Error::message)?, - )?; - for c in collabs { - e.str(c)?; - } - } - Ok(()) - }, + SupportedField::Collabs(collabs) => collabs.encode(e, ctx), SupportedField::Section(section) => section.encode(e, ctx), SupportedField::ContentEncoding(content_encoding) => content_encoding.encode(e, ctx), } diff --git a/rust/signed_doc/src/metadata/utils.rs b/rust/signed_doc/src/metadata/utils.rs deleted file mode 100644 index f2df23eb81..0000000000 --- a/rust/signed_doc/src/metadata/utils.rs +++ /dev/null @@ -1,97 +0,0 @@ -//! Utility functions for metadata decoding fields - -use catalyst_types::{ - problem_report::ProblemReport, - uuid::{CborContext, UuidV7}, -}; -use coset::{CborSerializable, Label, ProtectedHeader}; -use minicbor::{Decode, Decoder}; - -/// Decode cose protected header value using minicbor decoder. -pub(crate) fn decode_cose_protected_header_value( - protected: &ProtectedHeader, context: &mut C, label: &str, -) -> Option -where T: for<'a> Decode<'a, C> { - cose_protected_header_find(protected, |key| matches!(key, Label::Text(l) if l == label)) - .and_then(|value| { - let bytes = value.clone().to_vec().unwrap_or_default(); - Decoder::new(&bytes).decode_with(context).ok() - }) -} - -/// Find a value for a predicate in the protected header. -pub(crate) fn cose_protected_header_find( - protected: &coset::ProtectedHeader, mut predicate: impl FnMut(&coset::Label) -> bool, -) -> Option<&coset::cbor::Value> { - protected - .header - .rest - .iter() - .find(|(key, _)| predicate(key)) - .map(|(_, value)| value) -} - -/// Tries to decode field by the `field_name` from the COSE protected header -pub(crate) fn decode_document_field_from_protected_header( - protected: &ProtectedHeader, field_name: &str, report_content: &str, report: &ProblemReport, -) -> Option -where T: for<'a> TryFrom<&'a coset::cbor::Value> { - if let Some(cbor_doc_field) = - cose_protected_header_find(protected, |key| key == &Label::Text(field_name.to_string())) - { - if let Ok(field) = T::try_from(cbor_doc_field) { - return Some(field); - } - report.conversion_error( - &format!("CBOR COSE protected header {field_name}"), - &format!("{cbor_doc_field:?}"), - "Expected a CBOR UUID", - &format!("{report_content}, decoding CBOR UUID for {field_name}",), - ); - } - None -} - -/// A convenient wrapper over the `UuidV7` type, to implement -/// `TryFrom` and `TryFrom for coset::cbor::Value` traits. -pub(crate) struct CborUuidV7(pub(crate) UuidV7); -impl TryFrom<&coset::cbor::Value> for CborUuidV7 { - type Error = anyhow::Error; - - fn try_from(value: &coset::cbor::Value) -> Result { - Ok(Self(decode_cbor_uuid(value)?)) - } -} -impl TryFrom for coset::cbor::Value { - type Error = anyhow::Error; - - fn try_from(value: CborUuidV7) -> Result { - encode_cbor_uuid(value.0) - } -} - -/// Encode `uuid::Uuid` type into `coset::cbor::Value`. -/// -/// This is used to encode `UuidV4` and `UuidV7` types. -fn encode_cbor_uuid>( - value: T, -) -> anyhow::Result { - let mut cbor_bytes = Vec::new(); - minicbor::encode_with(value, &mut cbor_bytes, &mut CborContext::Tagged) - .map_err(|e| anyhow::anyhow!("Unable to encode CBOR value, err: {e}"))?; - coset::cbor::Value::from_slice(&cbor_bytes) - .map_err(|e| anyhow::anyhow!("Invalid CBOR value, err: {e}")) -} - -/// Decode `From` type from `coset::cbor::Value`. -/// -/// This is used to decode `UuidV4` and `UuidV7` types. -fn decode_cbor_uuid minicbor::decode::Decode<'a, CborContext>>( - value: &coset::cbor::Value, -) -> anyhow::Result { - let mut cbor_bytes = Vec::new(); - coset::cbor::ser::into_writer(value, &mut cbor_bytes) - .map_err(|e| anyhow::anyhow!("Invalid CBOR value, err: {e}"))?; - minicbor::decode_with(&cbor_bytes, &mut CborContext::Tagged) - .map_err(|e| anyhow::anyhow!("Invalid UUID, err: {e}")) -} diff --git a/rust/signed_doc/src/signature/mod.rs b/rust/signed_doc/src/signature/mod.rs index 5452707ffb..8806a0aae4 100644 --- a/rust/signed_doc/src/signature/mod.rs +++ b/rust/signed_doc/src/signature/mod.rs @@ -1,10 +1,8 @@ //! Catalyst Signed Document COSE Signature information. pub use catalyst_types::catalyst_id::CatalystId; -use catalyst_types::problem_report::ProblemReport; -use coset::CoseSignature; -use crate::{Content, Metadata}; +use crate::{decode_context::DecodeContext, Content, Metadata}; /// Catalyst Signed Document COSE Signature. #[derive(Debug, Clone)] @@ -30,33 +28,6 @@ impl Signature { pub fn signature(&self) -> &[u8] { &self.signature } - - /// Convert COSE Signature to `Signature`. - pub(crate) fn from_cose_sig(signature: CoseSignature, report: &ProblemReport) -> Option { - match CatalystId::try_from(signature.protected.header.key_id.as_ref()) { - Ok(kid) if kid.is_uri() => Some(Self::new(kid, signature.signature)), - Ok(kid) => { - report.invalid_value( - "COSE signature protected header key ID", - &kid.to_string(), - &format!( - "COSE signature protected header key ID must be a Catalyst ID, missing URI schema {}", CatalystId::SCHEME - ), - "Converting COSE signature header key ID to CatalystId", - ); - None - }, - Err(e) => { - report.conversion_error( - "COSE signature protected header key ID", - &format!("{:?}", &signature.protected.header.key_id), - &format!("{e:?}"), - "Converting COSE signature header key ID to CatalystId", - ); - None - }, - } - } } /// List of Signatures. @@ -85,23 +56,6 @@ impl Signatures { pub fn is_empty(&self) -> bool { self.0.is_empty() } - - /// Convert list of COSE Signature to `Signatures`. - pub(crate) fn from_cose_sig_list(cose_sigs: &[CoseSignature], report: &ProblemReport) -> Self { - let res = cose_sigs - .iter() - .cloned() - .enumerate() - .filter_map(|(idx, signature)| { - let sign = Signature::from_cose_sig(signature, report); - if sign.is_none() { - report.other(&format!("COSE signature protected header key ID at id {idx}"), "Converting COSE signatures list to Catalyst Signed Documents signatures list",); - } - sign - }).collect(); - - Self(res) - } } /// Create a binary blob that will be signed. No support for unprotected headers. @@ -114,7 +68,7 @@ pub(crate) fn tbs_data( // The context string as per [RFC 8152 section 4.4](https://datatracker.ietf.org/doc/html/rfc8152#section-4.4). "Signature", ::from(minicbor::to_vec(metadata)?), - ::from(protected_header_bytes(kid)?), + ::from(protected_header_encode(kid)?), minicbor::bytes::ByteArray::from([]), content, ))?) @@ -126,7 +80,7 @@ impl minicbor::Encode<()> for Signature { ) -> Result<(), minicbor::encode::Error> { e.array(3)?; e.bytes( - protected_header_bytes(&self.kid) + protected_header_encode(&self.kid) .map_err(minicbor::encode::Error::message)? .as_slice(), )?; @@ -137,6 +91,40 @@ impl minicbor::Encode<()> for Signature { } } +impl minicbor::Decode<'_, DecodeContext<'_>> for Option { + fn decode( + d: &mut minicbor::Decoder<'_>, ctx: &mut DecodeContext<'_>, + ) -> Result { + if !matches!(d.array()?, Some(3)) { + return Err(minicbor::decode::Error::message( + "COSE signature object must be a definite size array with 3 elements", + )); + } + + let kid = + protected_header_decode(d.bytes()?, ctx).map_err(minicbor::decode::Error::message)?; + + // empty unprotected headers + let mut map = + cbork_utils::deterministic_helper::decode_map_deterministically(d)?.into_iter(); + if map.next().is_some() { + ctx.report.unknown_field( + "unprotected headers", + "non empty unprotected headers", + "COSE signature unprotected headers must be empty", + ); + } + + let signature = d.bytes()?.to_vec(); + + if let Some(kid) = kid { + Ok(Some(Signature { kid, signature })) + } else { + Ok(None) + } + } +} + impl minicbor::Encode<()> for Signatures { fn encode( &self, e: &mut minicbor::Encoder, _ctx: &mut (), @@ -154,12 +142,96 @@ impl minicbor::Encode<()> for Signatures { } } +impl minicbor::Decode<'_, DecodeContext<'_>> for Signatures { + fn decode( + d: &mut minicbor::Decoder<'_>, ctx: &mut DecodeContext<'_>, + ) -> Result { + let Some(signatures_len) = d.array()? else { + return Err(minicbor::decode::Error::message( + "COSE signatures array must be a definite size array", + )); + }; + + let mut signatures = Vec::new(); + for idx in 0..signatures_len { + match d.decode_with(ctx)? { + Some(signature) => signatures.push(signature), + None => { + ctx.report.other( + &format!("COSE signature at id {idx}"), + "Cannot decode a single COSE signature from the array of signatures", + ); + }, + } + } + + Ok(Signatures(signatures)) + } +} + /// Signatures protected header bytes /// /// Described in [section 3.1 of RFC 8152](https://datatracker.ietf.org/doc/html/rfc8152#section-3.1). -fn protected_header_bytes(kid: &CatalystId) -> anyhow::Result> { - let mut p_headers = minicbor::Encoder::new(Vec::new()); +fn protected_header_encode(kid: &CatalystId) -> anyhow::Result> { + let mut p_header = minicbor::Encoder::new(Vec::new()); // protected headers (kid field) - p_headers.map(1)?.u8(4)?.encode(kid)?; - Ok(p_headers.into_writer()) + p_header + .map(1)? + .u8(4)? + .bytes(Vec::::from(kid).as_slice())?; + Ok(p_header.into_writer()) +} + +/// Signatures protected header decode from bytes. +/// Return error if its an invalid CBOR sequence. +/// Return None if cannot decode `CatalystId` bytes. +/// +/// Described in [section 3.1 of RFC 8152](https://datatracker.ietf.org/doc/html/rfc8152#section-3.1). +fn protected_header_decode( + bytes: &[u8], ctx: &mut DecodeContext<'_>, +) -> anyhow::Result> { + let mut map = cbork_utils::deterministic_helper::decode_map_deterministically( + &mut minicbor::Decoder::new(bytes), + )? + .into_iter(); + + let Some(entry) = map.next() else { + anyhow::bail!("COSE signature protected header must be at least one entry"); + }; + + // protected headers (kid field) + anyhow::ensure!( + matches!( + minicbor::Decoder::new(entry.key_bytes.as_slice()).u8(), + Ok(4) + ), + "Missing COSE signature protected header `kid` field" + ); + + let kid = minicbor::Decoder::new(entry.value.as_slice()) + .bytes()? + .try_into() + .inspect_err(|e| { + ctx.report.conversion_error( + "COSE signature protected header `kid`", + &hex::encode(entry.value.as_slice()), + &format!("{e:?}"), + "Converting COSE signature header `kid` to CatalystId", + ); + }) + .ok() + .inspect(|kid: &CatalystId| { + if kid.is_id() { + ctx.report.invalid_value( + "COSE signature protected header key ID", + &kid.to_string(), + &format!( + "COSE signature protected header key ID must be a Catalyst ID, missing URI schema {}", + CatalystId::SCHEME + ), + "Converting COSE signature header key ID to CatalystId", + ); + } + }); + Ok(kid) } diff --git a/rust/signed_doc/tests/decoding.rs b/rust/signed_doc/tests/decoding.rs index ba7611e9ac..c0448c5fc2 100644 --- a/rust/signed_doc/tests/decoding.rs +++ b/rust/signed_doc/tests/decoding.rs @@ -175,7 +175,7 @@ fn signed_doc_with_all_fields_case() -> TestCase { e.array(3)?; // protected headers (kid field) let mut p_headers = minicbor::Encoder::new(Vec::new()); - p_headers.map(1)?.u8(4)?.encode(kid)?; + p_headers.map(1)?.u8(4)?.bytes(Vec::::from(&kid).as_slice())?; e.bytes(p_headers.into_writer().as_slice())?; e.map(0)?; e.bytes(&[1,2,3])?; diff --git a/rust/signed_doc/tests/signature.rs b/rust/signed_doc/tests/signature.rs index b675b19947..c7c39165d9 100644 --- a/rust/signed_doc/tests/signature.rs +++ b/rust/signed_doc/tests/signature.rs @@ -43,9 +43,13 @@ async fn single_signature_validation_test() { // case: has key let mut provider = TestVerifyingKeyProvider::default(); provider.add_pk(kid.clone(), pk); - assert!(validator::validate_signatures(&signed_doc, &provider) - .await - .unwrap()); + assert!( + validator::validate_signatures(&signed_doc, &provider) + .await + .unwrap(), + "{:?}", + signed_doc.problem_report() + ); // case: empty provider assert!(