From 9095fd3bb78993bea4013aac8ca9f2dcb9ff9869 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Mon, 23 Jun 2025 22:28:16 +0300 Subject: [PATCH 01/28] more tests --- rust/signed_doc/tests/comment.rs | 69 ++++++++++++++++++++++++++++- rust/signed_doc/tests/proposal.rs | 60 ++++++++++++++++++++++++- rust/signed_doc/tests/submission.rs | 61 ++++++++++++++++++++++++- 3 files changed, 187 insertions(+), 3 deletions(-) diff --git a/rust/signed_doc/tests/comment.rs b/rust/signed_doc/tests/comment.rs index 16f9364694b..0d41851a820 100644 --- a/rust/signed_doc/tests/comment.rs +++ b/rust/signed_doc/tests/comment.rs @@ -5,8 +5,16 @@ use std::sync::LazyLock; use catalyst_signed_doc::{ - doc_types::deprecated, providers::tests::TestCatalystSignedDocumentProvider, *, + doc_types::deprecated, + providers::tests::{TestCatalystSignedDocumentProvider, TestVerifyingKeyProvider}, + *, }; +use catalyst_types::catalyst_id::role_index::RoleId; +use ed25519_dalek::ed25519::signature::Signer; + +use crate::common::create_dummy_key_pair; + +mod common; #[allow(clippy::unwrap_used)] static DUMMY_PROPOSAL_DOC: LazyLock = LazyLock::new(|| { @@ -107,6 +115,10 @@ static COMMENT_REF_DOC: LazyLock = LazyLock::new(|| { // The rule requires that the `ref` field in `ref_doc` must match the `ref` field in `doc` #[tokio::test] async fn test_valid_comment_doc() { + let (sk, pk, kid) = create_dummy_key_pair(RoleId::Role0).unwrap(); + let mut key_provider = TestVerifyingKeyProvider::default(); + key_provider.add_pk(kid.clone(), pk); + // Create a main comment doc, contain all fields mention in the document (except // revocations and section) let doc = Builder::new() @@ -136,6 +148,8 @@ async fn test_valid_comment_doc() { .unwrap() .with_json_content(&serde_json::json!({})) .unwrap() + .add_signature(|m| sk.sign(&m).to_vec(), kid) + .unwrap() .build() .unwrap(); @@ -147,6 +161,59 @@ async fn test_valid_comment_doc() { let is_valid = validator::validate(&doc, &provider).await.unwrap(); assert!(is_valid, "{:?}", doc.problem_report()); + + let is_valid = validator::validate_signatures(&doc, &key_provider) + .await + .unwrap(); + assert!(is_valid); +} + +#[tokio::test] +async fn test_invalid_comment_doc_wrong_role() { + let (sk, _pk, kid) = create_dummy_key_pair(RoleId::Proposer).unwrap(); + + // Create a main comment doc, contain all fields mention in the document (except + // revocations and section) + let doc = Builder::new() + .with_json_metadata(serde_json::json!({ + "content-type": ContentType::Json.to_string(), + "content-encoding": ContentEncoding::Brotli.to_string(), + "type": doc_types::PROPOSAL_COMMENT.clone(), + "id": UuidV7::new(), + "ver": UuidV7::new(), + "ref": { + "id": DUMMY_PROPOSAL_DOC.doc_id().unwrap(), + "ver": DUMMY_PROPOSAL_DOC.doc_ver().unwrap(), + }, + "template": { + "id": COMMENT_TEMPLATE_DOC.doc_id().unwrap(), + "ver": COMMENT_TEMPLATE_DOC.doc_ver().unwrap(), + }, + "reply": { + "id": COMMENT_REF_DOC.doc_id().unwrap(), + "ver": COMMENT_REF_DOC.doc_ver().unwrap() + }, + "parameters": { + "id": DUMMY_BRAND_DOC.doc_id().unwrap(), + "ver": DUMMY_BRAND_DOC.doc_ver().unwrap(), + } + })) + .unwrap() + .with_json_content(&serde_json::json!({})) + .unwrap() + .add_signature(|m| sk.sign(&m).to_vec(), kid) + .unwrap() + .build() + .unwrap(); + + let mut provider = TestCatalystSignedDocumentProvider::default(); + provider.add_document(None, &DUMMY_BRAND_DOC).unwrap(); + provider.add_document(None, &DUMMY_PROPOSAL_DOC).unwrap(); + provider.add_document(None, &COMMENT_REF_DOC).unwrap(); + provider.add_document(None, &COMMENT_TEMPLATE_DOC).unwrap(); + + let is_valid = validator::validate(&doc, &provider).await.unwrap(); + assert!(!is_valid, "{:?}", doc.problem_report()); } // The same as above but test with the old type diff --git a/rust/signed_doc/tests/proposal.rs b/rust/signed_doc/tests/proposal.rs index e2f129dca29..2f6074b0dcc 100644 --- a/rust/signed_doc/tests/proposal.rs +++ b/rust/signed_doc/tests/proposal.rs @@ -5,8 +5,16 @@ use std::sync::LazyLock; use catalyst_signed_doc::{ - doc_types::deprecated, providers::tests::TestCatalystSignedDocumentProvider, *, + doc_types::deprecated, + providers::tests::{TestCatalystSignedDocumentProvider, TestVerifyingKeyProvider}, + *, }; +use catalyst_types::catalyst_id::role_index::RoleId; +use ed25519_dalek::ed25519::signature::Signer; + +use crate::common::create_dummy_key_pair; + +mod common; #[allow(clippy::unwrap_used)] static DUMMY_BRAND_DOC: LazyLock = LazyLock::new(|| { @@ -59,6 +67,10 @@ static PROPOSAL_TEMPLATE_DOC: LazyLock = LazyLock::new(| // `parameters` value as `doc`. #[tokio::test] async fn test_valid_proposal_doc() { + let (sk, pk, kid) = create_dummy_key_pair(RoleId::Proposer).unwrap(); + let mut key_provider = TestVerifyingKeyProvider::default(); + key_provider.add_pk(kid.clone(), pk); + // Create a main proposal doc, contain all fields mention in the document (except // collaborations and revocations) let doc = Builder::new() @@ -80,6 +92,8 @@ async fn test_valid_proposal_doc() { .unwrap() .with_json_content(&serde_json::json!({})) .unwrap() + .add_signature(|m| sk.sign(&m).to_vec(), kid) + .unwrap() .build() .unwrap(); @@ -90,6 +104,50 @@ async fn test_valid_proposal_doc() { let is_valid = validator::validate(&doc, &provider).await.unwrap(); assert!(is_valid); + + let is_valid = validator::validate_signatures(&doc, &key_provider) + .await + .unwrap(); + assert!(is_valid); +} + +#[tokio::test] +async fn test_ivalid_proposal_doc_wrong_role() { + let (sk, _pk, kid) = create_dummy_key_pair(RoleId::Role0).unwrap(); + + // Create a main proposal doc, contain all fields mention in the document (except + // collaborations and revocations) + let doc = Builder::new() + .with_json_metadata(serde_json::json!({ + "content-type": ContentType::Json.to_string(), + "content-encoding": ContentEncoding::Brotli.to_string(), + "type": doc_types::PROPOSAL.clone(), + "id": UuidV7::new(), + "ver": UuidV7::new(), + "template": { + "id": PROPOSAL_TEMPLATE_DOC.doc_id().unwrap(), + "ver": PROPOSAL_TEMPLATE_DOC.doc_ver().unwrap(), + }, + "parameters": { + "id": DUMMY_BRAND_DOC.doc_id().unwrap(), + "ver": DUMMY_BRAND_DOC.doc_ver().unwrap(), + } + })) + .unwrap() + .with_json_content(&serde_json::json!({})) + .unwrap() + .add_signature(|m| sk.sign(&m).to_vec(), kid) + .unwrap() + .build() + .unwrap(); + + let mut provider = TestCatalystSignedDocumentProvider::default(); + + provider.add_document(None, &PROPOSAL_TEMPLATE_DOC).unwrap(); + provider.add_document(None, &DUMMY_BRAND_DOC).unwrap(); + + let is_valid = validator::validate(&doc, &provider).await.unwrap(); + assert!(!is_valid); } #[tokio::test] diff --git a/rust/signed_doc/tests/submission.rs b/rust/signed_doc/tests/submission.rs index a4199d064ae..6723770bf1f 100644 --- a/rust/signed_doc/tests/submission.rs +++ b/rust/signed_doc/tests/submission.rs @@ -5,8 +5,16 @@ use std::sync::LazyLock; use catalyst_signed_doc::{ - doc_types::deprecated, providers::tests::TestCatalystSignedDocumentProvider, *, + doc_types::deprecated, + providers::tests::{TestCatalystSignedDocumentProvider, TestVerifyingKeyProvider}, + *, }; +use catalyst_types::catalyst_id::role_index::RoleId; +use ed25519_dalek::ed25519::signature::Signer; + +use crate::common::create_dummy_key_pair; + +mod common; #[allow(clippy::unwrap_used)] static DUMMY_PROPOSAL_DOC: LazyLock = LazyLock::new(|| { @@ -51,6 +59,10 @@ static DUMMY_BRAND_DOC: LazyLock = LazyLock::new(|| { // `parameters` value as `doc`. #[tokio::test] async fn test_valid_submission_action() { + let (sk, pk, kid) = create_dummy_key_pair(RoleId::Proposer).unwrap(); + let mut key_provider = TestVerifyingKeyProvider::default(); + key_provider.add_pk(kid.clone(), pk); + // Create a main proposal submission doc, contain all fields mention in the document let doc = Builder::new() .with_json_metadata(serde_json::json!({ @@ -73,6 +85,8 @@ async fn test_valid_submission_action() { "action": "final" })) .unwrap() + .add_signature(|m| sk.sign(&m).to_vec(), kid) + .unwrap() .build() .unwrap(); @@ -83,6 +97,51 @@ async fn test_valid_submission_action() { let is_valid = validator::validate(&doc, &provider).await.unwrap(); assert!(is_valid, "{:?}", doc.problem_report()); + + let is_valid = validator::validate_signatures(&doc, &key_provider) + .await + .unwrap(); + assert!(is_valid); +} + +#[tokio::test] +async fn test_invalid_submission_action_wrong_role() { + let (sk, _pk, kid) = create_dummy_key_pair(RoleId::Role0).unwrap(); + + // Create a main proposal submission doc, contain all fields mention in the document + let doc = Builder::new() + .with_json_metadata(serde_json::json!({ + "content-type": ContentType::Json.to_string(), + "content-encoding": ContentEncoding::Brotli.to_string(), + "type": doc_types::PROPOSAL_SUBMISSION_ACTION.clone(), + "id": UuidV7::new(), + "ver": UuidV7::new(), + "ref": { + "id": DUMMY_PROPOSAL_DOC.doc_id().unwrap(), + "ver": DUMMY_PROPOSAL_DOC.doc_ver().unwrap(), + }, + "parameters": { + "id": DUMMY_BRAND_DOC.doc_id().unwrap(), + "ver": DUMMY_BRAND_DOC.doc_ver().unwrap(), + } + })) + .unwrap() + .with_json_content(&serde_json::json!({ + "action": "final" + })) + .unwrap() + .add_signature(|m| sk.sign(&m).to_vec(), kid) + .unwrap() + .build() + .unwrap(); + + let mut provider = TestCatalystSignedDocumentProvider::default(); + + provider.add_document(None, &DUMMY_PROPOSAL_DOC).unwrap(); + provider.add_document(None, &DUMMY_BRAND_DOC).unwrap(); + + let is_valid = validator::validate(&doc, &provider).await.unwrap(); + assert!(!is_valid); } #[tokio::test] From a02282348036bcbe98eaf06a8b8348218caaea3d Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Mon, 23 Jun 2025 23:18:25 +0300 Subject: [PATCH 02/28] add signatures decoding logic --- rust/catalyst-types/src/catalyst_id/mod.rs | 9 +- rust/cbork-utils/src/deterministic_helper.rs | 28 +++-- rust/signed_doc/Cargo.toml | 1 + rust/signed_doc/src/signature/mod.rs | 108 +++++++++++++++++-- 4 files changed, 124 insertions(+), 22 deletions(-) diff --git a/rust/catalyst-types/src/catalyst_id/mod.rs b/rust/catalyst-types/src/catalyst_id/mod.rs index 1612cec4a18..0a51516fa53 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 b2855c9e6a0..ad8e61e4176 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 @@ -123,10 +122,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 +497,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 151de0a34f7..63b66870701 100644 --- a/rust/signed_doc/Cargo.toml +++ b/rust/signed_doc/Cargo.toml @@ -12,6 +12,7 @@ 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"] } diff --git a/rust/signed_doc/src/signature/mod.rs b/rust/signed_doc/src/signature/mod.rs index 5452707ffb6..564cd2b2a35 100644 --- a/rust/signed_doc/src/signature/mod.rs +++ b/rust/signed_doc/src/signature/mod.rs @@ -4,7 +4,7 @@ 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)] @@ -114,7 +114,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 +126,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 +137,43 @@ impl minicbor::Encode<()> for Signature { } } +impl minicbor::Decode<'_, DecodeContext<'_>> for Signature { + 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()?).map_err(minicbor::decode::Error::message)?; + + 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", + ); + } + + // empty unprotected headers + if !matches!(d.map()?, Some(0)) { + return Err(minicbor::decode::Error::message( + "COSE signature unprotected headers must be a definite size empty map", + )); + } + + let signature = d.bytes()?.to_vec(); + + Ok(Self { kid, signature }) + } +} + impl minicbor::Encode<()> for Signatures { fn encode( &self, e: &mut minicbor::Encoder, _ctx: &mut (), @@ -154,12 +191,69 @@ 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) { + Ok(signature) => signatures.push(signature), + Err(e) => { + ctx.report.other( + &format!("COSE signature at id {idx}, error: {e}"), + "Cannot decode a signle 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_header + .map(1)? + .u8(4)? + .bytes(Vec::::from(kid).as_slice())?; + Ok(p_header.into_writer()) +} + +/// Signatures protected header decode from 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]) -> 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) - p_headers.map(1)?.u8(4)?.encode(kid)?; - Ok(p_headers.into_writer()) + anyhow::ensure!( + matches!( + minicbor::Decoder::new(entry.key_bytes.as_slice()).u8(), + Ok(4) + ), + "Missing COSE signature protected header `kid` field" + ); + let kid: CatalystId = minicbor::Decoder::new(entry.key_bytes.as_slice()) + .bytes()? + .try_into()?; + Ok(kid) } From d6616775369e76a4386e33ac520f91ed647b764e Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Tue, 24 Jun 2025 09:12:29 +0300 Subject: [PATCH 03/28] wip --- rust/signed_doc/src/metadata/section.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/rust/signed_doc/src/metadata/section.rs b/rust/signed_doc/src/metadata/section.rs index f4d4834415e..5d250dfdc41 100644 --- a/rust/signed_doc/src/metadata/section.rs +++ b/rust/signed_doc/src/metadata/section.rs @@ -59,3 +59,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) + } +} From e682d018007f4b627a6993909bdb15f88eee0649 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Mon, 23 Jun 2025 23:35:37 +0300 Subject: [PATCH 04/28] wip --- rust/signed_doc/src/content.rs | 10 ++++++++++ .../src/metadata/content_encoding.rs | 8 ++++++++ rust/signed_doc/src/metadata/content_type.rs | 19 +++++++++++++++++++ 3 files changed, 37 insertions(+) diff --git a/rust/signed_doc/src/content.rs b/rust/signed_doc/src/content.rs index 30ed78174ed..6e801d75460 100644 --- a/rust/signed_doc/src/content.rs +++ b/rust/signed_doc/src/content.rs @@ -36,3 +36,13 @@ impl minicbor::Encode<()> for Content { Ok(()) } } + +impl minicbor::Decode<'_, ()> for Content { + fn decode( + d: &mut minicbor::Decoder<'_>, _ctx: &mut (), + ) -> Result { + d.null() + .map(|()| Self(Vec::new())) + .or(d.bytes().map(Vec::from).map(Self)) + } +} diff --git a/rust/signed_doc/src/metadata/content_encoding.rs b/rust/signed_doc/src/metadata/content_encoding.rs index e736dc74d60..ed696015795 100644 --- a/rust/signed_doc/src/metadata/content_encoding.rs +++ b/rust/signed_doc/src/metadata/content_encoding.rs @@ -93,3 +93,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 260c0196b51..5c0088e6e69 100644 --- a/rust/signed_doc/src/metadata/content_type.rs +++ b/rust/signed_doc/src/metadata/content_type.rs @@ -86,6 +86,25 @@ impl minicbor::Encode<()> for ContentType { } } +impl minicbor::Decode<'_, ()> for ContentType { + fn decode( + d: &mut minicbor::Decoder<'_>, _ctx: &mut (), + ) -> Result { + 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.str()?.parse().map_err(minicbor::decode::Error::message), + } + } +} + #[cfg(test)] mod tests { use super::*; From 813c9b818a94e48ba9473029aacb105259e6d93e Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Tue, 24 Jun 2025 09:43:22 +0300 Subject: [PATCH 05/28] wip --- rust/signed_doc/src/signature/mod.rs | 66 ++++++++++++++++++---------- 1 file changed, 42 insertions(+), 24 deletions(-) diff --git a/rust/signed_doc/src/signature/mod.rs b/rust/signed_doc/src/signature/mod.rs index 564cd2b2a35..eede9e9fac1 100644 --- a/rust/signed_doc/src/signature/mod.rs +++ b/rust/signed_doc/src/signature/mod.rs @@ -137,7 +137,7 @@ impl minicbor::Encode<()> for Signature { } } -impl minicbor::Decode<'_, DecodeContext<'_>> for Signature { +impl minicbor::Decode<'_, DecodeContext<'_>> for Option { fn decode( d: &mut minicbor::Decoder<'_>, ctx: &mut DecodeContext<'_>, ) -> Result { @@ -147,19 +147,8 @@ impl minicbor::Decode<'_, DecodeContext<'_>> for Signature { )); } - let kid = protected_header_decode(d.bytes()?).map_err(minicbor::decode::Error::message)?; - - 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", - ); - } + let kid = + protected_header_decode(d.bytes()?, ctx).map_err(minicbor::decode::Error::message)?; // empty unprotected headers if !matches!(d.map()?, Some(0)) { @@ -170,7 +159,11 @@ impl minicbor::Decode<'_, DecodeContext<'_>> for Signature { let signature = d.bytes()?.to_vec(); - Ok(Self { kid, signature }) + if let Some(kid) = kid { + Ok(Some(Signature { kid, signature })) + } else { + Ok(None) + } } } @@ -203,11 +196,11 @@ impl minicbor::Decode<'_, DecodeContext<'_>> for Signatures { let mut signatures = Vec::new(); for idx in 0..signatures_len { - match d.decode_with(ctx) { - Ok(signature) => signatures.push(signature), - Err(e) => { + match d.decode_with(ctx)? { + Some(signature) => signatures.push(signature), + None => { ctx.report.other( - &format!("COSE signature at id {idx}, error: {e}"), + &format!("COSE signature at id {idx}"), "Cannot decode a signle COSE signature from the array of signatures", ); }, @@ -232,9 +225,13 @@ fn protected_header_encode(kid: &CatalystId) -> anyhow::Result> { } /// 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]) -> anyhow::Result { +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), )? @@ -252,8 +249,29 @@ fn protected_header_decode(bytes: &[u8]) -> anyhow::Result { ), "Missing COSE signature protected header `kid` field" ); - let kid: CatalystId = minicbor::Decoder::new(entry.key_bytes.as_slice()) - .bytes()? - .try_into()?; - Ok(kid) + Ok(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", + ) + }).map(|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", + ); + } + kid + }) + .ok()) } From befe239f0722ae69349456c7bf3bf220d22d9780 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Tue, 24 Jun 2025 10:04:18 +0300 Subject: [PATCH 06/28] wip --- rust/cbork-utils/src/deterministic_helper.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/rust/cbork-utils/src/deterministic_helper.rs b/rust/cbork-utils/src/deterministic_helper.rs index ad8e61e4176..c801e5d0b4f 100644 --- a/rust/cbork-utils/src/deterministic_helper.rs +++ b/rust/cbork-utils/src/deterministic_helper.rs @@ -74,8 +74,7 @@ impl Ord for MapEntry { } } -/// Decodes a CBOR map with deterministic encoding validation (RFC 8949 Section 4.2.3) -/// Returns the raw bytes of the map if it passes all deterministic validation rules. +/// Decodes a CBOR map with deterministic encoding validation (RFC 8949 Section 4.2) /// /// From RFC 8949 Section 4.2.3: /// "The keys in every map must be sorted in the following order: From 2225029344d7dc548ebdde67cbf0de5ecf02cc8a Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Tue, 24 Jun 2025 10:07:50 +0300 Subject: [PATCH 07/28] wip --- rust/signed_doc/tests/decoding.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/signed_doc/tests/decoding.rs b/rust/signed_doc/tests/decoding.rs index 04da22a0635..71e584c6c69 100644 --- a/rust/signed_doc/tests/decoding.rs +++ b/rust/signed_doc/tests/decoding.rs @@ -63,7 +63,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])?; From 8b19f4d9dd233983123333884aa56d1cc7cac00c Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Tue, 24 Jun 2025 10:08:01 +0300 Subject: [PATCH 08/28] wip --- rust/signed_doc/src/signature/mod.rs | 43 ++++++++++++++++------------ 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/rust/signed_doc/src/signature/mod.rs b/rust/signed_doc/src/signature/mod.rs index eede9e9fac1..c21167b92f5 100644 --- a/rust/signed_doc/src/signature/mod.rs +++ b/rust/signed_doc/src/signature/mod.rs @@ -151,9 +151,11 @@ impl minicbor::Decode<'_, DecodeContext<'_>> for Option { protected_header_decode(d.bytes()?, ctx).map_err(minicbor::decode::Error::message)?; // empty unprotected headers - if !matches!(d.map()?, Some(0)) { + let mut map = + cbork_utils::deterministic_helper::decode_map_deterministically(d)?.into_iter(); + if map.next().is_some() { return Err(minicbor::decode::Error::message( - "COSE signature unprotected headers must be a definite size empty map", + "COSE signature unprotected headers must be empty", )); } @@ -249,19 +251,22 @@ fn protected_header_decode( ), "Missing COSE signature protected header `kid` field" ); - Ok(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", - ) - }).map(|kid: CatalystId| { - if kid.is_id() { - ctx.report.invalid_value( + + 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() + .map(|kid: CatalystId| { + if kid.is_id() { + ctx.report.invalid_value( "COSE signature protected header key ID", &kid.to_string(), &format!( @@ -270,8 +275,8 @@ fn protected_header_decode( ), "Converting COSE signature header key ID to CatalystId", ); - } - kid - }) - .ok()) + } + kid + }); + Ok(kid) } From fd8000d91675f6285e085caff1b09d59b3f7b1cf Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Tue, 24 Jun 2025 12:19:14 +0300 Subject: [PATCH 09/28] wip --- rust/signed_doc/src/metadata/mod.rs | 87 +++++-------------- .../src/metadata/supported_field.rs | 67 +++++++++----- rust/signed_doc/tests/signature.rs | 10 ++- 3 files changed, 70 insertions(+), 94 deletions(-) diff --git a/rust/signed_doc/src/metadata/mod.rs b/rust/signed_doc/src/metadata/mod.rs index 878e9e72aad..8ad05328831 100644 --- a/rust/signed_doc/src/metadata/mod.rs +++ b/rust/signed_doc/src/metadata/mod.rs @@ -1,7 +1,6 @@ //! Catalyst Signed Document Metadata. use std::{ - collections::{btree_map, BTreeMap}, - error::Error, + collections::HashMap, fmt::{Display, Formatter}, }; @@ -58,7 +57,7 @@ const CATEGORY_ID_KEY: &str = "category_id"; /// /// These values are extracted from the COSE Sign protected header. #[derive(Clone, Debug, PartialEq, Default)] -pub struct Metadata(BTreeMap); +pub struct Metadata(HashMap); impl Metadata { /// Return Document Type `DocType` - a list of `UUIDv4`. @@ -182,7 +181,7 @@ impl Metadata { pub(crate) fn from_fields(fields: Vec, report: &ProblemReport) -> Self { const REPORT_CONTEXT: &str = "Metadata building"; - let mut metadata = Metadata(BTreeMap::new()); + let mut metadata = Metadata(HashMap::new()); for v in fields { let k = v.discriminant(); if metadata.0.insert(k, v).is_some() { @@ -430,27 +429,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. /// @@ -466,51 +444,26 @@ impl minicbor::Decode<'_, crate::decode_context::DecodeContext<'_>> for Metadata ) -> Result { const REPORT_CONTEXT: &str = "Metadata decoding"; - let Some(len) = d.map()? else { - return Err(minicbor::decode::Error::message( - "Indefinite map is not supported", - )); - }; - - // 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 mut metadata_map = HashMap::new(); + let map = cbork_utils::deterministic_helper::decode_map_deterministically(d)?.into_iter(); + for entry in map { + let entry_bytes = [entry.key_bytes, entry.value].concat(); + let Some(field) = + Option::::decode(&mut minicbor::Decoder::new(&entry_bytes), ctx)? + else { + continue; + }; + let field_label = field.discriminant(); + if metadata_map.insert(field_label, field).is_some() { + ctx.report.duplicate_field( + &field_label.to_string(), + "Duplicate metadata fields are not allowed", + REPORT_CONTEXT, + ); } } - first_err.map_or(Ok(Self(metadata_map)), Err) + Ok(Self(metadata_map)) } } diff --git a/rust/signed_doc/src/metadata/supported_field.rs b/rust/signed_doc/src/metadata/supported_field.rs index 99572c4d7d7..6efc289d74d 100644 --- a/rust/signed_doc/src/metadata/supported_field.rs +++ b/rust/signed_doc/src/metadata/supported_field.rs @@ -8,10 +8,7 @@ use catalyst_types::uuid::UuidV7; use serde::Deserialize; use strum::{EnumDiscriminants, EnumTryAs, IntoDiscriminant as _}; -use crate::{ - metadata::custom_transient_decode_error, ContentEncoding, ContentType, DocType, DocumentRefs, - Section, -}; +use crate::{ContentEncoding, ContentType, DocType, DocumentRefs, Section}; /// COSE label. May be either a signed integer or a string. #[derive(Copy, Clone, Eq, PartialEq)] @@ -89,7 +86,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)) )] @@ -197,14 +194,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 +213,44 @@ 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(SupportedField::Type), + SupportedLabel::Reply => d.decode_with(ctx).map(SupportedField::Reply), + SupportedLabel::Collabs => { + collabs_decode(&mut d) + .map_err(minicbor::decode::Error::message) + .map(SupportedField::Collabs) }, - 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::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) } @@ -285,6 +293,17 @@ impl minicbor::Encode<()> for SupportedField { } } +fn collabs_decode(d: &mut minicbor::Decoder<'_>) -> anyhow::Result> { + let Some(items) = d.array()? else { + anyhow::bail!("Must a definite size array"); + }; + let collabs = (0..items) + .into_iter() + .map(|_| Ok(d.str()?.to_string())) + .collect::>()?; + Ok(collabs) +} + #[cfg(test)] mod tests { use strum::VariantArray as _; diff --git a/rust/signed_doc/tests/signature.rs b/rust/signed_doc/tests/signature.rs index b675b19947b..c7c39165d98 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!( From 61a5a5f0c79a0c46b5cf2f8bc90db3b30eda6b22 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Tue, 24 Jun 2025 14:21:48 +0300 Subject: [PATCH 10/28] wip --- rust/signed_doc/src/metadata/mod.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rust/signed_doc/src/metadata/mod.rs b/rust/signed_doc/src/metadata/mod.rs index 8ad05328831..d3c23bbdf24 100644 --- a/rust/signed_doc/src/metadata/mod.rs +++ b/rust/signed_doc/src/metadata/mod.rs @@ -1,6 +1,6 @@ //! Catalyst Signed Document Metadata. use std::{ - collections::HashMap, + collections::BTreeMap, fmt::{Display, Formatter}, }; @@ -57,7 +57,7 @@ const CATEGORY_ID_KEY: &str = "category_id"; /// /// These values are extracted from the COSE Sign protected header. #[derive(Clone, Debug, PartialEq, Default)] -pub struct Metadata(HashMap); +pub struct Metadata(BTreeMap); impl Metadata { /// Return Document Type `DocType` - a list of `UUIDv4`. @@ -181,7 +181,7 @@ impl Metadata { pub(crate) fn from_fields(fields: Vec, report: &ProblemReport) -> Self { const REPORT_CONTEXT: &str = "Metadata building"; - let mut metadata = Metadata(HashMap::new()); + let mut metadata = Metadata(BTreeMap::new()); for v in fields { let k = v.discriminant(); if metadata.0.insert(k, v).is_some() { @@ -444,7 +444,7 @@ impl minicbor::Decode<'_, crate::decode_context::DecodeContext<'_>> for Metadata ) -> Result { const REPORT_CONTEXT: &str = "Metadata decoding"; - let mut metadata_map = HashMap::new(); + let mut metadata_map = BTreeMap::new(); let map = cbork_utils::deterministic_helper::decode_map_deterministically(d)?.into_iter(); for entry in map { let entry_bytes = [entry.key_bytes, entry.value].concat(); From 51195ab60a8357a8462ca10e7c91c480d9c5883d Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Tue, 24 Jun 2025 15:02:13 +0300 Subject: [PATCH 11/28] wip --- rust/signed_doc/src/lib.rs | 59 ++++---- rust/signed_doc/src/metadata/mod.rs | 194 -------------------------- rust/signed_doc/src/metadata/utils.rs | 53 +------ rust/signed_doc/src/signature/mod.rs | 46 ------ 4 files changed, 36 insertions(+), 316 deletions(-) diff --git a/rust/signed_doc/src/lib.rs b/rust/signed_doc/src/lib.rs index f9b67abd585..e620c8defb4 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"; +/// +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,35 +211,45 @@ impl CatalystSignedDocument { impl Decode<'_, ()> for CatalystSignedDocument { fn decode(d: &mut Decoder<'_>, _ctx: &mut ()) -> Result { + let mut report = ProblemReport::new("Catalyst Signed Document Decoding"); + let mut ctx = DecodeContext { + compatibility_policy: CompatibilityPolicy::Accept, + report: &mut report, + }; let start = d.position(); - d.skip()?; + + 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}" + ))); + } + } + if !matches!(d.array()?, Some(4)) { + return Err(minicbor::decode::Error::message(format!( + "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() { + return Err(minicbor::decode::Error::message( + "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())?; - 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 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 content = if let Some(payload) = cose_sign.payload { - payload.into() - } else { - report.missing_field("COSE Sign Payload", "Missing document content (payload)"); - Content::default() - }; - Ok(InnerCatalystSignedDocument { metadata, content, diff --git a/rust/signed_doc/src/metadata/mod.rs b/rust/signed_doc/src/metadata/mod.rs index d3c23bbdf24..7599cf7c237 100644 --- a/rust/signed_doc/src/metadata/mod.rs +++ b/rust/signed_doc/src/metadata/mod.rs @@ -20,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. /// @@ -219,170 +189,6 @@ impl 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 {{")?; diff --git a/rust/signed_doc/src/metadata/utils.rs b/rust/signed_doc/src/metadata/utils.rs index f2df23eb81e..e923e8a441c 100644 --- a/rust/signed_doc/src/metadata/utils.rs +++ b/rust/signed_doc/src/metadata/utils.rs @@ -1,56 +1,7 @@ //! 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 -} +use catalyst_types::uuid::{CborContext, UuidV7}; +use coset::CborSerializable; /// A convenient wrapper over the `UuidV7` type, to implement /// `TryFrom` and `TryFrom for coset::cbor::Value` traits. diff --git a/rust/signed_doc/src/signature/mod.rs b/rust/signed_doc/src/signature/mod.rs index c21167b92f5..2d942ede875 100644 --- a/rust/signed_doc/src/signature/mod.rs +++ b/rust/signed_doc/src/signature/mod.rs @@ -1,8 +1,6 @@ //! Catalyst Signed Document COSE Signature information. pub use catalyst_types::catalyst_id::CatalystId; -use catalyst_types::problem_report::ProblemReport; -use coset::CoseSignature; use crate::{decode_context::DecodeContext, Content, Metadata}; @@ -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. From af8bbdd46a72add98dab558f5b46c4e2a04047b6 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Tue, 24 Jun 2025 15:08:19 +0300 Subject: [PATCH 12/28] wip --- rust/signed_doc/src/content.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rust/signed_doc/src/content.rs b/rust/signed_doc/src/content.rs index 6e801d75460..052c5ff3b48 100644 --- a/rust/signed_doc/src/content.rs +++ b/rust/signed_doc/src/content.rs @@ -43,6 +43,7 @@ impl minicbor::Decode<'_, ()> for Content { ) -> Result { d.null() .map(|()| Self(Vec::new())) - .or(d.bytes().map(Vec::from).map(Self)) + // important to use `or_else` so it will lazy evaluated at the time when it is needed + .or_else(|_| d.bytes().map(Vec::from).map(Self)) } } From a600ae159689204c1d239f2a2978d1c4dd8d96da Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Tue, 24 Jun 2025 15:26:20 +0300 Subject: [PATCH 13/28] fix --- rust/signed_doc/src/content.rs | 9 ++++++- rust/signed_doc/src/lib.rs | 6 +++++ rust/signed_doc/src/metadata/content_type.rs | 28 ++++---------------- 3 files changed, 19 insertions(+), 24 deletions(-) diff --git a/rust/signed_doc/src/content.rs b/rust/signed_doc/src/content.rs index 052c5ff3b48..33dc43595e5 100644 --- a/rust/signed_doc/src/content.rs +++ b/rust/signed_doc/src/content.rs @@ -41,9 +41,16 @@ 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.bytes().map(Vec::from).map(Self)) + .or_else(|_| { + d.set_position(p); + d.bytes() + .map_err(|_| minicbor::decode::Error::message("fuck you")) + .map(Vec::from) + .map(Self) + }) } } diff --git a/rust/signed_doc/src/lib.rs b/rust/signed_doc/src/lib.rs index e620c8defb4..f592ec8ebdb 100644 --- a/rust/signed_doc/src/lib.rs +++ b/rust/signed_doc/src/lib.rs @@ -218,20 +218,26 @@ impl Decode<'_, ()> for CatalystSignedDocument { }; let start = d.position(); + let p = d.position(); 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 { + d.set_position(p); } + if !matches!(d.array()?, Some(4)) { return Err(minicbor::decode::Error::message(format!( "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(); diff --git a/rust/signed_doc/src/metadata/content_type.rs b/rust/signed_doc/src/metadata/content_type.rs index 5c0088e6e69..d22dcad2da7 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 (), @@ -90,6 +68,7 @@ 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), @@ -100,7 +79,10 @@ impl minicbor::Decode<'_, ()> for ContentType { "unsupported CoAP Content Formats value: {val}" ))) }, - Err(_) => d.str()?.parse().map_err(minicbor::decode::Error::message), + Err(_) => { + d.set_position(p); + d.str()?.parse().map_err(minicbor::decode::Error::message) + }, } } } From 09901d3bc27ac1f3336a528cbe0570f166b03e7a Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Tue, 24 Jun 2025 17:50:06 +0300 Subject: [PATCH 14/28] wip --- .../src/metadata/document_refs/doc_ref.rs | 47 +----------------- .../src/metadata/document_refs/mod.rs | 42 ---------------- rust/signed_doc/src/metadata/mod.rs | 1 - rust/signed_doc/src/metadata/utils.rs | 48 ------------------- rust/signed_doc/tests/decoding.rs | 1 + 5 files changed, 3 insertions(+), 136 deletions(-) delete mode 100644 rust/signed_doc/src/metadata/utils.rs 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 2339fc6450e..0923e9414a3 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 3c3cf6704f5..022dc1c3c21 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 7599cf7c237..518f0360e49 100644 --- a/rust/signed_doc/src/metadata/mod.rs +++ b/rust/signed_doc/src/metadata/mod.rs @@ -10,7 +10,6 @@ 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; diff --git a/rust/signed_doc/src/metadata/utils.rs b/rust/signed_doc/src/metadata/utils.rs deleted file mode 100644 index e923e8a441c..00000000000 --- a/rust/signed_doc/src/metadata/utils.rs +++ /dev/null @@ -1,48 +0,0 @@ -//! Utility functions for metadata decoding fields - -use catalyst_types::uuid::{CborContext, UuidV7}; -use coset::CborSerializable; - -/// 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/tests/decoding.rs b/rust/signed_doc/tests/decoding.rs index 4abd9c212aa..673795e0c67 100644 --- a/rust/signed_doc/tests/decoding.rs +++ b/rust/signed_doc/tests/decoding.rs @@ -141,6 +141,7 @@ fn signed_doc_with_all_fields_case() -> TestCase { } #[test] +#[ignore] fn catalyst_signed_doc_decoding_test() { let test_cases = [ decoding_empty_bytes_case(), From 7aacebb4fd8292e41a1781b2bce38ab52b83e599 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Tue, 24 Jun 2025 17:54:50 +0300 Subject: [PATCH 15/28] remove coset --- rust/signed_doc/Cargo.toml | 1 - .../src/metadata/content_encoding.rs | 13 -------- rust/signed_doc/src/metadata/doc_type.rs | 32 +------------------ .../src/metadata/document_refs/doc_locator.rs | 25 --------------- rust/signed_doc/src/metadata/section.rs | 12 ------- 5 files changed, 1 insertion(+), 82 deletions(-) diff --git a/rust/signed_doc/Cargo.toml b/rust/signed_doc/Cargo.toml index 63b66870701..1c1f75fe940 100644 --- a/rust/signed_doc/Cargo.toml +++ b/rust/signed_doc/Cargo.toml @@ -17,7 +17,6 @@ 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/metadata/content_encoding.rs b/rust/signed_doc/src/metadata/content_encoding.rs index ed696015795..3a9ff1ad76b 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 (), diff --git a/rust/signed_doc/src/metadata/doc_type.rs b/rust/signed_doc/src/metadata/doc_type.rs index c55a45be5e9..adf09faa649 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 61e6bac229f..303f4624d21 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/section.rs b/rust/signed_doc/src/metadata/section.rs index 5d250dfdc41..cbe97fee3da 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 (), From bb996cd7fd7d6b552297bfe3e7dab4479725e25b Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Tue, 24 Jun 2025 18:10:52 +0300 Subject: [PATCH 16/28] fix clippy --- rust/signed_doc/src/lib.rs | 6 +++--- rust/signed_doc/src/metadata/supported_field.rs | 2 +- rust/signed_doc/src/signature/mod.rs | 5 ++--- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/rust/signed_doc/src/lib.rs b/rust/signed_doc/src/lib.rs index f592ec8ebdb..00cbe91385e 100644 --- a/rust/signed_doc/src/lib.rs +++ b/rust/signed_doc/src/lib.rs @@ -230,9 +230,9 @@ impl Decode<'_, ()> for CatalystSignedDocument { } if !matches!(d.array()?, Some(4)) { - return Err(minicbor::decode::Error::message(format!( - "Must be a definite size array of 4 elements" - ))); + return Err(minicbor::decode::Error::message( + "Must be a definite size array of 4 elements", + )); } let metadata_bytes = d.bytes()?; diff --git a/rust/signed_doc/src/metadata/supported_field.rs b/rust/signed_doc/src/metadata/supported_field.rs index 6efc289d74d..60708320220 100644 --- a/rust/signed_doc/src/metadata/supported_field.rs +++ b/rust/signed_doc/src/metadata/supported_field.rs @@ -293,12 +293,12 @@ impl minicbor::Encode<()> for SupportedField { } } +/// `collabs` CBOR decode. fn collabs_decode(d: &mut minicbor::Decoder<'_>) -> anyhow::Result> { let Some(items) = d.array()? else { anyhow::bail!("Must a definite size array"); }; let collabs = (0..items) - .into_iter() .map(|_| Ok(d.str()?.to_string())) .collect::>()?; Ok(collabs) diff --git a/rust/signed_doc/src/signature/mod.rs b/rust/signed_doc/src/signature/mod.rs index 2d942ede875..e691d8db4c1 100644 --- a/rust/signed_doc/src/signature/mod.rs +++ b/rust/signed_doc/src/signature/mod.rs @@ -215,10 +215,10 @@ fn protected_header_decode( &hex::encode(entry.value.as_slice()), &format!("{e:?}"), "Converting COSE signature header `kid` to CatalystId", - ) + ); }) .ok() - .map(|kid: CatalystId| { + .inspect(|kid: &CatalystId| { if kid.is_id() { ctx.report.invalid_value( "COSE signature protected header key ID", @@ -230,7 +230,6 @@ fn protected_header_decode( "Converting COSE signature header key ID to CatalystId", ); } - kid }); Ok(kid) } From 6e009c941d53b97c1147637470c6268117c75636 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Tue, 24 Jun 2025 19:17:20 +0300 Subject: [PATCH 17/28] wip --- rust/cbork-utils/src/decode_helper.rs | 109 +++++++++++++++++++ rust/cbork-utils/src/deterministic_helper.rs | 94 +--------------- rust/signed_doc/src/metadata/mod.rs | 13 ++- rust/signed_doc/tests/decoding.rs | 1 - 4 files changed, 123 insertions(+), 94 deletions(-) diff --git a/rust/cbork-utils/src/decode_helper.rs b/rust/cbork-utils/src/decode_helper.rs index 1a8ab480f1e..35a989f1ee0 100644 --- a/rust/cbork-utils/src/decode_helper.rs +++ b/rust/cbork-utils/src/decode_helper.rs @@ -1,7 +1,116 @@ //! CBOR decoding helper functions. +use std::cmp::Ordering; + use minicbor::{data::Tag, decode, Decoder}; +/// Represents a CBOR map key-value pair. +#[derive(Clone, Eq, PartialEq, Debug)] +pub struct MapEntry { + /// Raw bytes of the encoded key + pub key_bytes: Vec, + /// Raw bytes of the encoded value + pub value: Vec, +} + +impl PartialOrd for MapEntry { + /// Compare map entries according to RFC 8949 Section 4.2.3 rules: + /// 1. Compare by length of encoded key + /// 2. If lengths equal, compare byte wise lexicographically + /// + /// Returns Some(ordering) since comparison is always defined for these types + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for MapEntry { + /// Compare map entries according to RFC 8949 Section 4.2.3 rules: + /// 1. Compare by length of encoded key + /// 2. If lengths equal, compare byte wise lexicographically + fn cmp(&self, other: &Self) -> Ordering { + self.key_bytes + .len() + .cmp(&other.key_bytes.len()) + .then_with(|| self.key_bytes.cmp(&other.key_bytes)) + } +} + +/// Decodes a definite size CBOR map +/// +/// Additionally: +/// - Indefinite-length maps are not allowed (Section 4.2.2) +/// +/// # Errors +/// - Indefinite size map +/// - Decoding errors +pub fn decode_map(d: &mut Decoder) -> Result, minicbor::decode::Error> { + // From RFC 8949 Section 4.2.2: + // "Indefinite-length items must be made definite-length items." + // The specification explicitly prohibits indefinite-length items in + // deterministic encoding to ensure consistent representation. + let map_len = d.map()?.ok_or_else(|| { + minicbor::decode::Error::message( + "Indefinite-length items must be made definite-length items", + ) + })?; + + // Decode entries to validate them + let entries = decode_map_entries(d, map_len, |_| Ok(()))?; + + Ok(entries) +} + +/// Decodes all key-value pairs in the map +pub(crate) fn decode_map_entries( + d: &mut Decoder, length: u64, key_check: impl Fn(&[u8]) -> Result<(), minicbor::decode::Error>, +) -> Result, minicbor::decode::Error> { + let capacity = usize::try_from(length).map_err(|_| { + minicbor::decode::Error::message("Map length too large for current platform") + })?; + let mut entries = Vec::with_capacity(capacity); + + // Decode each key-value pair + for _ in 0..length { + // Record the starting position of the key + let key_start = d.position(); + + // Skip over the key to find its end position + d.skip()?; + let key_end = d.position(); + + // Record the starting position of the value + let value_start = d.position(); + + // Skip over the value to find its end position + d.skip()?; + let value_end = d.position(); + + // The keys themselves must be deterministically encoded (4.2.1) + let key_bytes = get_bytes(d, key_start, key_end)?; + key_check(&key_bytes)?; + let value = get_bytes(d, value_start, value_end)?; + + entries.push(MapEntry { key_bytes, value }); + } + + Ok(entries) +} + +/// Extracts the raw bytes of a CBOR map from a decoder based on specified positions. +/// This function retrieves the raw byte representation of a CBOR map between the given +/// start and end positions from the decoder's underlying buffer. +fn get_bytes( + d: &Decoder<'_>, map_start: usize, map_end: usize, +) -> Result, minicbor::decode::Error> { + d.input() + .get(map_start..map_end) + .ok_or_else(|| { + minicbor::decode::Error::message("Invalid map byte range: indices out of bounds") + }) + .map(<[u8]>::to_vec) +} + /// Generic helper function for decoding different types. /// /// # Errors diff --git a/rust/cbork-utils/src/deterministic_helper.rs b/rust/cbork-utils/src/deterministic_helper.rs index c801e5d0b4f..88b4571376e 100644 --- a/rust/cbork-utils/src/deterministic_helper.rs +++ b/rust/cbork-utils/src/deterministic_helper.rs @@ -14,6 +14,8 @@ use std::cmp::Ordering; use minicbor::Decoder; +use crate::decode_helper::{decode_map_entries, MapEntry}; + /// Major type indicator for CBOR maps (major type 5: 101 in top 3 bits) /// As per RFC 8949 Section 4.2.3, maps in deterministic encoding must: /// - Have keys sorted by length first, then byte wise lexicographically @@ -36,44 +38,6 @@ const CBOR_MAX_TINY_VALUE: u64 = 23; /// Used when encoding CBOR maps with lengths between 24 and 255 elements. const CBOR_MAP_LENGTH_UINT8: u8 = CBOR_MAJOR_TYPE_MAP | 24; // For uint8 length encoding -/// Represents a CBOR map key-value pair where the key must be deterministically encoded -/// according to RFC 8949 Section 4.2.3. -/// -/// This type stores the raw bytes of both key and value to enable: -/// 1. Length-first ordering of keys (shorter keys before longer ones) -/// 2. Lexicographic comparison of equal-length keys -/// 3. Preservation of the original encoded form -#[derive(Clone, Eq, PartialEq, Debug)] -pub struct MapEntry { - /// Raw bytes of the encoded key, used for deterministic ordering - pub key_bytes: Vec, - /// Raw bytes of the encoded value - pub value: Vec, -} - -impl PartialOrd for MapEntry { - /// Compare map entries according to RFC 8949 Section 4.2.3 rules: - /// 1. Compare by length of encoded key - /// 2. If lengths equal, compare byte wise lexicographically - /// - /// Returns Some(ordering) since comparison is always defined for these types - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for MapEntry { - /// Compare map entries according to RFC 8949 Section 4.2.3 rules: - /// 1. Compare by length of encoded key - /// 2. If lengths equal, compare byte wise lexicographically - fn cmp(&self, other: &Self) -> Ordering { - self.key_bytes - .len() - .cmp(&other.key_bytes.len()) - .then_with(|| self.key_bytes.cmp(&other.key_bytes)) - } -} - /// Decodes a CBOR map with deterministic encoding validation (RFC 8949 Section 4.2) /// /// From RFC 8949 Section 4.2.3: @@ -113,68 +77,16 @@ pub fn decode_map_deterministically( })?; let header_end_pos = d.position(); - check_map_minimal_length(d, header_end_pos, map_len)?; // Decode entries to validate them - let entries = decode_map_entries(d, map_len)?; + let entries = decode_map_entries(d, map_len, map_keys_are_deterministic)?; validate_map_ordering(&entries)?; Ok(entries) } -/// Extracts the raw bytes of a CBOR map from a decoder based on specified positions. -/// This function retrieves the raw byte representation of a CBOR map between the given -/// start and end positions from the decoder's underlying buffer. -fn get_bytes( - d: &Decoder<'_>, map_start: usize, map_end: usize, -) -> Result, minicbor::decode::Error> { - d.input() - .get(map_start..map_end) - .ok_or_else(|| { - minicbor::decode::Error::message("Invalid map byte range: indices out of bounds") - }) - .map(<[u8]>::to_vec) -} - -/// Decodes all key-value pairs in the map -fn decode_map_entries( - d: &mut Decoder, length: u64, -) -> Result, minicbor::decode::Error> { - let capacity = usize::try_from(length).map_err(|_| { - minicbor::decode::Error::message("Map length too large for current platform") - })?; - let mut entries = Vec::with_capacity(capacity); - - // Decode each key-value pair - for _ in 0..length { - // Record the starting position of the key - let key_start = d.position(); - - // Skip over the key to find its end position - d.skip()?; - let key_end = d.position(); - - // Record the starting position of the value - let value_start = d.position(); - - // Skip over the value to find its end position - d.skip()?; - let value_end = d.position(); - - // The keys themselves must be deterministically encoded (4.2.1) - let key_bytes = get_bytes(d, key_start, key_end)?; - map_keys_are_deterministic(&key_bytes)?; - - let value = get_bytes(d, value_start, value_end)?; - - entries.push(MapEntry { key_bytes, value }); - } - - Ok(entries) -} - /// Validates that a CBOR map key follows the deterministic encoding rules as specified in /// RFC 8949. In this case, it validates that the keys themselves must be /// deterministically encoded (4.2.1). diff --git a/rust/signed_doc/src/metadata/mod.rs b/rust/signed_doc/src/metadata/mod.rs index 518f0360e49..6fb80bebf59 100644 --- a/rust/signed_doc/src/metadata/mod.rs +++ b/rust/signed_doc/src/metadata/mod.rs @@ -20,6 +20,7 @@ use minicbor::Decoder; pub use section::Section; use strum::IntoDiscriminant as _; +use crate::decode_context::CompatibilityPolicy; pub(crate) use crate::metadata::supported_field::{SupportedField, SupportedLabel}; /// Document Metadata. @@ -248,9 +249,17 @@ impl minicbor::Decode<'_, crate::decode_context::DecodeContext<'_>> for Metadata d: &mut Decoder<'_>, ctx: &mut crate::decode_context::DecodeContext<'_>, ) -> Result { const REPORT_CONTEXT: &str = "Metadata decoding"; - let mut metadata_map = BTreeMap::new(); - let map = cbork_utils::deterministic_helper::decode_map_deterministically(d)?.into_iter(); + + let map = match ctx.compatibility_policy { + CompatibilityPolicy::Accept | CompatibilityPolicy::Warn => { + cbork_utils::decode_helper::decode_map(d)?.into_iter() + }, + CompatibilityPolicy::Fail => { + cbork_utils::deterministic_helper::decode_map_deterministically(d)?.into_iter() + }, + }; + for entry in map { let entry_bytes = [entry.key_bytes, entry.value].concat(); let Some(field) = diff --git a/rust/signed_doc/tests/decoding.rs b/rust/signed_doc/tests/decoding.rs index 673795e0c67..4abd9c212aa 100644 --- a/rust/signed_doc/tests/decoding.rs +++ b/rust/signed_doc/tests/decoding.rs @@ -141,7 +141,6 @@ fn signed_doc_with_all_fields_case() -> TestCase { } #[test] -#[ignore] fn catalyst_signed_doc_decoding_test() { let test_cases = [ decoding_empty_bytes_case(), From 120273bfe29b25a707298e1430517215cb3078a1 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Tue, 24 Jun 2025 19:22:10 +0300 Subject: [PATCH 18/28] fix spelling --- rust/signed_doc/src/signature/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/signed_doc/src/signature/mod.rs b/rust/signed_doc/src/signature/mod.rs index e691d8db4c1..9c7a9e6b012 100644 --- a/rust/signed_doc/src/signature/mod.rs +++ b/rust/signed_doc/src/signature/mod.rs @@ -157,7 +157,7 @@ impl minicbor::Decode<'_, DecodeContext<'_>> for Signatures { None => { ctx.report.other( &format!("COSE signature at id {idx}"), - "Cannot decode a signle COSE signature from the array of signatures", + "Cannot decode a single COSE signature from the array of signatures", ); }, } From b93c6949d53c9231b1ad81250942b0ad9d550d5e Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Wed, 25 Jun 2025 09:37:46 +0300 Subject: [PATCH 19/28] wip --- rust/signed_doc/src/metadata/mod.rs | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/rust/signed_doc/src/metadata/mod.rs b/rust/signed_doc/src/metadata/mod.rs index 6fb80bebf59..331ff024a8c 100644 --- a/rust/signed_doc/src/metadata/mod.rs +++ b/rust/signed_doc/src/metadata/mod.rs @@ -20,7 +20,6 @@ use minicbor::Decoder; pub use section::Section; use strum::IntoDiscriminant as _; -use crate::decode_context::CompatibilityPolicy; pub(crate) use crate::metadata::supported_field::{SupportedField, SupportedLabel}; /// Document Metadata. @@ -251,20 +250,14 @@ impl minicbor::Decode<'_, crate::decode_context::DecodeContext<'_>> for Metadata const REPORT_CONTEXT: &str = "Metadata decoding"; let mut metadata_map = BTreeMap::new(); - let map = match ctx.compatibility_policy { - CompatibilityPolicy::Accept | CompatibilityPolicy::Warn => { - cbork_utils::decode_helper::decode_map(d)?.into_iter() - }, - CompatibilityPolicy::Fail => { - cbork_utils::deterministic_helper::decode_map_deterministically(d)?.into_iter() - }, + let Some(length) = d.map()? else { + return Err(minicbor::decode::Error::message( + "COSE protected headers object must be a definite size map ", + )); }; - for entry in map { - let entry_bytes = [entry.key_bytes, entry.value].concat(); - let Some(field) = - Option::::decode(&mut minicbor::Decoder::new(&entry_bytes), ctx)? - else { + for _ in 0..length { + let Some(field) = Option::::decode(d, ctx)? else { continue; }; let field_label = field.discriminant(); From 34fa732d9bc421276aa96f80351d3118007816a4 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Wed, 25 Jun 2025 09:40:40 +0300 Subject: [PATCH 20/28] wip --- rust/cbork-utils/src/decode_helper.rs | 109 ------------------- rust/cbork-utils/src/deterministic_helper.rs | 96 +++++++++++++++- 2 files changed, 92 insertions(+), 113 deletions(-) diff --git a/rust/cbork-utils/src/decode_helper.rs b/rust/cbork-utils/src/decode_helper.rs index 35a989f1ee0..1a8ab480f1e 100644 --- a/rust/cbork-utils/src/decode_helper.rs +++ b/rust/cbork-utils/src/decode_helper.rs @@ -1,116 +1,7 @@ //! CBOR decoding helper functions. -use std::cmp::Ordering; - use minicbor::{data::Tag, decode, Decoder}; -/// Represents a CBOR map key-value pair. -#[derive(Clone, Eq, PartialEq, Debug)] -pub struct MapEntry { - /// Raw bytes of the encoded key - pub key_bytes: Vec, - /// Raw bytes of the encoded value - pub value: Vec, -} - -impl PartialOrd for MapEntry { - /// Compare map entries according to RFC 8949 Section 4.2.3 rules: - /// 1. Compare by length of encoded key - /// 2. If lengths equal, compare byte wise lexicographically - /// - /// Returns Some(ordering) since comparison is always defined for these types - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for MapEntry { - /// Compare map entries according to RFC 8949 Section 4.2.3 rules: - /// 1. Compare by length of encoded key - /// 2. If lengths equal, compare byte wise lexicographically - fn cmp(&self, other: &Self) -> Ordering { - self.key_bytes - .len() - .cmp(&other.key_bytes.len()) - .then_with(|| self.key_bytes.cmp(&other.key_bytes)) - } -} - -/// Decodes a definite size CBOR map -/// -/// Additionally: -/// - Indefinite-length maps are not allowed (Section 4.2.2) -/// -/// # Errors -/// - Indefinite size map -/// - Decoding errors -pub fn decode_map(d: &mut Decoder) -> Result, minicbor::decode::Error> { - // From RFC 8949 Section 4.2.2: - // "Indefinite-length items must be made definite-length items." - // The specification explicitly prohibits indefinite-length items in - // deterministic encoding to ensure consistent representation. - let map_len = d.map()?.ok_or_else(|| { - minicbor::decode::Error::message( - "Indefinite-length items must be made definite-length items", - ) - })?; - - // Decode entries to validate them - let entries = decode_map_entries(d, map_len, |_| Ok(()))?; - - Ok(entries) -} - -/// Decodes all key-value pairs in the map -pub(crate) fn decode_map_entries( - d: &mut Decoder, length: u64, key_check: impl Fn(&[u8]) -> Result<(), minicbor::decode::Error>, -) -> Result, minicbor::decode::Error> { - let capacity = usize::try_from(length).map_err(|_| { - minicbor::decode::Error::message("Map length too large for current platform") - })?; - let mut entries = Vec::with_capacity(capacity); - - // Decode each key-value pair - for _ in 0..length { - // Record the starting position of the key - let key_start = d.position(); - - // Skip over the key to find its end position - d.skip()?; - let key_end = d.position(); - - // Record the starting position of the value - let value_start = d.position(); - - // Skip over the value to find its end position - d.skip()?; - let value_end = d.position(); - - // The keys themselves must be deterministically encoded (4.2.1) - let key_bytes = get_bytes(d, key_start, key_end)?; - key_check(&key_bytes)?; - let value = get_bytes(d, value_start, value_end)?; - - entries.push(MapEntry { key_bytes, value }); - } - - Ok(entries) -} - -/// Extracts the raw bytes of a CBOR map from a decoder based on specified positions. -/// This function retrieves the raw byte representation of a CBOR map between the given -/// start and end positions from the decoder's underlying buffer. -fn get_bytes( - d: &Decoder<'_>, map_start: usize, map_end: usize, -) -> Result, minicbor::decode::Error> { - d.input() - .get(map_start..map_end) - .ok_or_else(|| { - minicbor::decode::Error::message("Invalid map byte range: indices out of bounds") - }) - .map(<[u8]>::to_vec) -} - /// Generic helper function for decoding different types. /// /// # Errors diff --git a/rust/cbork-utils/src/deterministic_helper.rs b/rust/cbork-utils/src/deterministic_helper.rs index 88b4571376e..7c68692f277 100644 --- a/rust/cbork-utils/src/deterministic_helper.rs +++ b/rust/cbork-utils/src/deterministic_helper.rs @@ -14,8 +14,6 @@ use std::cmp::Ordering; use minicbor::Decoder; -use crate::decode_helper::{decode_map_entries, MapEntry}; - /// Major type indicator for CBOR maps (major type 5: 101 in top 3 bits) /// As per RFC 8949 Section 4.2.3, maps in deterministic encoding must: /// - Have keys sorted by length first, then byte wise lexicographically @@ -38,7 +36,46 @@ const CBOR_MAX_TINY_VALUE: u64 = 23; /// Used when encoding CBOR maps with lengths between 24 and 255 elements. const CBOR_MAP_LENGTH_UINT8: u8 = CBOR_MAJOR_TYPE_MAP | 24; // For uint8 length encoding -/// Decodes a CBOR map with deterministic encoding validation (RFC 8949 Section 4.2) +/// Represents a CBOR map key-value pair where the key must be deterministically encoded +/// according to RFC 8949 Section 4.2.3. +/// +/// This type stores the raw bytes of both key and value to enable: +/// 1. Length-first ordering of keys (shorter keys before longer ones) +/// 2. Lexicographic comparison of equal-length keys +/// 3. Preservation of the original encoded form +#[derive(Clone, Eq, PartialEq, Debug)] +pub struct MapEntry { + /// Raw bytes of the encoded key, used for deterministic ordering + pub key_bytes: Vec, + /// Raw bytes of the encoded value + pub value: Vec, +} + +impl PartialOrd for MapEntry { + /// Compare map entries according to RFC 8949 Section 4.2.3 rules: + /// 1. Compare by length of encoded key + /// 2. If lengths equal, compare byte wise lexicographically + /// + /// Returns Some(ordering) since comparison is always defined for these types + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for MapEntry { + /// Compare map entries according to RFC 8949 Section 4.2.3 rules: + /// 1. Compare by length of encoded key + /// 2. If lengths equal, compare byte wise lexicographically + fn cmp(&self, other: &Self) -> Ordering { + self.key_bytes + .len() + .cmp(&other.key_bytes.len()) + .then_with(|| self.key_bytes.cmp(&other.key_bytes)) + } +} + +/// Decodes a CBOR map with deterministic encoding validation (RFC 8949 Section 4.2.3) +/// Returns the raw bytes of the map if it passes all deterministic validation rules. /// /// From RFC 8949 Section 4.2.3: /// "The keys in every map must be sorted in the following order: @@ -80,13 +117,64 @@ pub fn decode_map_deterministically( check_map_minimal_length(d, header_end_pos, map_len)?; // Decode entries to validate them - let entries = decode_map_entries(d, map_len, map_keys_are_deterministic)?; + let entries = decode_map_entries(d, map_len)?; validate_map_ordering(&entries)?; Ok(entries) } +/// Extracts the raw bytes of a CBOR map from a decoder based on specified positions. +/// This function retrieves the raw byte representation of a CBOR map between the given +/// start and end positions from the decoder's underlying buffer. +fn get_bytes( + d: &Decoder<'_>, map_start: usize, map_end: usize, +) -> Result, minicbor::decode::Error> { + d.input() + .get(map_start..map_end) + .ok_or_else(|| { + minicbor::decode::Error::message("Invalid map byte range: indices out of bounds") + }) + .map(<[u8]>::to_vec) +} + +/// Decodes all key-value pairs in the map +fn decode_map_entries( + d: &mut Decoder, length: u64, +) -> Result, minicbor::decode::Error> { + let capacity = usize::try_from(length).map_err(|_| { + minicbor::decode::Error::message("Map length too large for current platform") + })?; + let mut entries = Vec::with_capacity(capacity); + + // Decode each key-value pair + for _ in 0..length { + // Record the starting position of the key + let key_start = d.position(); + + // Skip over the key to find its end position + d.skip()?; + let key_end = d.position(); + + // Record the starting position of the value + let value_start = d.position(); + + // Skip over the value to find its end position + d.skip()?; + let value_end = d.position(); + + // The keys themselves must be deterministically encoded (4.2.1) + let key_bytes = get_bytes(d, key_start, key_end)?; + map_keys_are_deterministic(&key_bytes)?; + + let value = get_bytes(d, value_start, value_end)?; + + entries.push(MapEntry { key_bytes, value }); + } + + Ok(entries) +} + /// Validates that a CBOR map key follows the deterministic encoding rules as specified in /// RFC 8949. In this case, it validates that the keys themselves must be /// deterministically encoded (4.2.1). From 379cc597e11af56e9e4f09ba730026fe10e51050 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Wed, 25 Jun 2025 09:44:55 +0300 Subject: [PATCH 21/28] wip --- rust/signed_doc/src/metadata/mod.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rust/signed_doc/src/metadata/mod.rs b/rust/signed_doc/src/metadata/mod.rs index 331ff024a8c..2f055a16a67 100644 --- a/rust/signed_doc/src/metadata/mod.rs +++ b/rust/signed_doc/src/metadata/mod.rs @@ -250,6 +250,9 @@ impl minicbor::Decode<'_, crate::decode_context::DecodeContext<'_>> for Metadata const REPORT_CONTEXT: &str = "Metadata decoding"; let mut metadata_map = BTreeMap::new(); + // TODO: use helpers from `cbork-utils` crate to verify that's the map is + // deterministically CBOR encoded map. + let Some(length) = d.map()? else { return Err(minicbor::decode::Error::message( "COSE protected headers object must be a definite size map ", From 11e99c449897eddd1f6dfa3bcecf5e91349f4861 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Wed, 25 Jun 2025 11:13:19 +0300 Subject: [PATCH 22/28] wip --- rust/signed_doc/src/content.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/rust/signed_doc/src/content.rs b/rust/signed_doc/src/content.rs index 33dc43595e5..1126b0711fb 100644 --- a/rust/signed_doc/src/content.rs +++ b/rust/signed_doc/src/content.rs @@ -47,10 +47,7 @@ impl minicbor::Decode<'_, ()> for Content { // 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_err(|_| minicbor::decode::Error::message("fuck you")) - .map(Vec::from) - .map(Self) + d.bytes().map(Vec::from).map(Self) }) } } From a0c24bf32f69973b38f42bb9b98792d2647ce6d0 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Wed, 25 Jun 2025 11:16:30 +0300 Subject: [PATCH 23/28] fix comment --- rust/signed_doc/src/lib.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/rust/signed_doc/src/lib.rs b/rust/signed_doc/src/lib.rs index 00cbe91385e..18a4b5d0f2d 100644 --- a/rust/signed_doc/src/lib.rs +++ b/rust/signed_doc/src/lib.rs @@ -218,7 +218,6 @@ impl Decode<'_, ()> for CatalystSignedDocument { }; let start = d.position(); - let p = d.position(); if let Ok(tag) = d.tag() { if tag != COSE_SIGN_CBOR_TAG { return Err(minicbor::decode::Error::message(format!( @@ -226,7 +225,7 @@ impl Decode<'_, ()> for CatalystSignedDocument { ))); } } else { - d.set_position(p); + d.set_position(start); } if !matches!(d.array()?, Some(4)) { From 8dbb31588b2149fa6c8c3e145de4f386037a4837 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Wed, 25 Jun 2025 13:27:33 +0300 Subject: [PATCH 24/28] wip --- rust/signed_doc/src/metadata/mod.rs | 34 ++++++++++++----------------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/rust/signed_doc/src/metadata/mod.rs b/rust/signed_doc/src/metadata/mod.rs index 2f055a16a67..d97f678b870 100644 --- a/rust/signed_doc/src/metadata/mod.rs +++ b/rust/signed_doc/src/metadata/mod.rs @@ -147,11 +147,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( @@ -175,14 +178,17 @@ 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(|v| anyhow::Result::<_>::Ok(v)), + &report, + )?; anyhow::ensure!(!report.is_problematic(), "{:?}", report); Ok(metadata) } @@ -247,9 +253,6 @@ 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"; - let mut metadata_map = BTreeMap::new(); - // TODO: use helpers from `cbork-utils` crate to verify that's the map is // deterministically CBOR encoded map. @@ -259,21 +262,12 @@ impl minicbor::Decode<'_, crate::decode_context::DecodeContext<'_>> for Metadata )); }; - for _ in 0..length { - let Some(field) = Option::::decode(d, ctx)? else { - continue; - }; - let field_label = field.discriminant(); - if metadata_map.insert(field_label, field).is_some() { - ctx.report.duplicate_field( - &field_label.to_string(), - "Duplicate metadata fields are not allowed", - REPORT_CONTEXT, - ); - } - } + let report = ctx.report.clone(); + let fields = (0..length) + .map(|_| Option::::decode(d, ctx)) + .filter_map(|v| v.transpose()); - Ok(Self(metadata_map)) + Self::from_fields(fields, &report) } } From d83460f1c3a36007829734ee9cc103e81b4eeda7 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Wed, 25 Jun 2025 20:38:56 +0300 Subject: [PATCH 25/28] add Collaborators type --- rust/signed_doc/src/metadata/collaborators.rs | 57 +++++++++++++++++++ rust/signed_doc/src/metadata/mod.rs | 4 +- .../src/metadata/supported_field.rs | 39 +++---------- 3 files changed, 67 insertions(+), 33 deletions(-) create mode 100644 rust/signed_doc/src/metadata/collaborators.rs diff --git a/rust/signed_doc/src/metadata/collaborators.rs b/rust/signed_doc/src/metadata/collaborators.rs new file mode 100644 index 00000000000..d5819cc0468 --- /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/mod.rs b/rust/signed_doc/src/metadata/mod.rs index d97f678b870..a907f3e0660 100644 --- a/rust/signed_doc/src/metadata/mod.rs +++ b/rust/signed_doc/src/metadata/mod.rs @@ -2,8 +2,10 @@ use std::{ collections::BTreeMap, fmt::{Display, Formatter}, + ops::Deref, }; +mod collaborators; mod content_encoding; mod content_type; pub(crate) mod doc_type; @@ -123,7 +125,7 @@ impl Metadata { self.0 .get(&SupportedLabel::Collabs) .and_then(SupportedField::try_as_collabs_ref) - .map_or(&[], Vec::as_slice) + .map_or(&[], |v| v.deref()) } /// Return `parameters` field. diff --git a/rust/signed_doc/src/metadata/supported_field.rs b/rust/signed_doc/src/metadata/supported_field.rs index 60708320220..46fb58c802b 100644 --- a/rust/signed_doc/src/metadata/supported_field.rs +++ b/rust/signed_doc/src/metadata/supported_field.rs @@ -8,7 +8,10 @@ use catalyst_types::uuid::UuidV7; use serde::Deserialize; use strum::{EnumDiscriminants, EnumTryAs, IntoDiscriminant as _}; -use crate::{ContentEncoding, ContentType, DocType, DocumentRefs, Section}; +use crate::{ + metadata::collaborators::Collaborators, ContentEncoding, ContentType, DocType, DocumentRefs, + Section, +}; /// COSE label. May be either a signed integer or a string. #[derive(Copy, Clone, Eq, PartialEq)] @@ -109,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. @@ -232,11 +235,7 @@ impl minicbor::Decode<'_, crate::decode_context::DecodeContext<'_>> for Option d.decode_with(ctx).map(SupportedField::Type), SupportedLabel::Reply => d.decode_with(ctx).map(SupportedField::Reply), - SupportedLabel::Collabs => { - collabs_decode(&mut d) - .map_err(minicbor::decode::Error::message) - .map(SupportedField::Collabs) - }, + 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), @@ -273,37 +272,13 @@ 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), } } } -/// `collabs` CBOR decode. -fn collabs_decode(d: &mut minicbor::Decoder<'_>) -> anyhow::Result> { - let Some(items) = d.array()? else { - anyhow::bail!("Must a definite size array"); - }; - let collabs = (0..items) - .map(|_| Ok(d.str()?.to_string())) - .collect::>()?; - Ok(collabs) -} - #[cfg(test)] mod tests { use strum::VariantArray as _; From 857410a141898fedbb4dbe327709d4652bb364b8 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Wed, 25 Jun 2025 20:43:33 +0300 Subject: [PATCH 26/28] wip --- rust/signed_doc/src/lib.rs | 6 ++++-- rust/signed_doc/src/signature/mod.rs | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/rust/signed_doc/src/lib.rs b/rust/signed_doc/src/lib.rs index 18a4b5d0f2d..6b04c6c5f04 100644 --- a/rust/signed_doc/src/lib.rs +++ b/rust/signed_doc/src/lib.rs @@ -241,9 +241,11 @@ impl Decode<'_, ()> for CatalystSignedDocument { let mut map = cbork_utils::deterministic_helper::decode_map_deterministically(d)?.into_iter(); if map.next().is_some() { - return Err(minicbor::decode::Error::message( + ctx.report.unknown_field( + "unprotected headers", + "non empty unprotected headers", "COSE unprotected headers must be empty", - )); + ); } let content = Content::decode(d, &mut ())?; diff --git a/rust/signed_doc/src/signature/mod.rs b/rust/signed_doc/src/signature/mod.rs index 9c7a9e6b012..8806a0aae4a 100644 --- a/rust/signed_doc/src/signature/mod.rs +++ b/rust/signed_doc/src/signature/mod.rs @@ -108,9 +108,11 @@ impl minicbor::Decode<'_, DecodeContext<'_>> for Option { let mut map = cbork_utils::deterministic_helper::decode_map_deterministically(d)?.into_iter(); if map.next().is_some() { - return Err(minicbor::decode::Error::message( + ctx.report.unknown_field( + "unprotected headers", + "non empty unprotected headers", "COSE signature unprotected headers must be empty", - )); + ); } let signature = d.bytes()?.to_vec(); From 88e2c63adaec35cf9bf9c30548b6d737bfa329ff Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Wed, 25 Jun 2025 20:47:58 +0300 Subject: [PATCH 27/28] wip --- rust/signed_doc/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/signed_doc/src/lib.rs b/rust/signed_doc/src/lib.rs index 6b04c6c5f04..f9187d11b53 100644 --- a/rust/signed_doc/src/lib.rs +++ b/rust/signed_doc/src/lib.rs @@ -30,7 +30,7 @@ pub use signature::{CatalystId, Signatures}; use crate::builder::SignaturesBuilder; -/// +/// 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. From ee9786929942196c94fa8b9e2782c440ea2de515 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Wed, 25 Jun 2025 21:04:32 +0300 Subject: [PATCH 28/28] fix clippy --- rust/signed_doc/src/lib.rs | 2 +- rust/signed_doc/src/metadata/mod.rs | 10 +++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/rust/signed_doc/src/lib.rs b/rust/signed_doc/src/lib.rs index f9187d11b53..3700f910a33 100644 --- a/rust/signed_doc/src/lib.rs +++ b/rust/signed_doc/src/lib.rs @@ -30,7 +30,7 @@ pub use signature::{CatalystId, Signatures}; use crate::builder::SignaturesBuilder; -/// COSE_Sign object CBOR tag +/// `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. diff --git a/rust/signed_doc/src/metadata/mod.rs b/rust/signed_doc/src/metadata/mod.rs index a907f3e0660..5b9f71f5033 100644 --- a/rust/signed_doc/src/metadata/mod.rs +++ b/rust/signed_doc/src/metadata/mod.rs @@ -2,7 +2,6 @@ use std::{ collections::BTreeMap, fmt::{Display, Formatter}, - ops::Deref, }; mod collaborators; @@ -125,7 +124,7 @@ impl Metadata { self.0 .get(&SupportedLabel::Collabs) .and_then(SupportedField::try_as_collabs_ref) - .map_or(&[], |v| v.deref()) + .map_or(&[], |v| &**v) } /// Return `parameters` field. @@ -187,10 +186,7 @@ impl Metadata { 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.into_iter().map(|v| anyhow::Result::<_>::Ok(v)), - &report, - )?; + let metadata = Self::from_fields(fields.into_iter().map(anyhow::Result::<_>::Ok), &report)?; anyhow::ensure!(!report.is_problematic(), "{:?}", report); Ok(metadata) } @@ -267,7 +263,7 @@ impl minicbor::Decode<'_, crate::decode_context::DecodeContext<'_>> for Metadata let report = ctx.report.clone(); let fields = (0..length) .map(|_| Option::::decode(d, ctx)) - .filter_map(|v| v.transpose()); + .filter_map(Result::transpose); Self::from_fields(fields, &report) }