From 9db5fe2a707fd849b831f2efeff4f4c36edc060d Mon Sep 17 00:00:00 2001 From: no30bit Date: Fri, 23 May 2025 17:59:29 +0300 Subject: [PATCH 01/16] content type fix & encoding/decoding --- rust/cbork-utils/src/decode_helper.rs | 29 ++++++ rust/signed_doc/Cargo.toml | 5 +- rust/signed_doc/src/metadata/content_type.rs | 100 ++++++------------- rust/signed_doc/src/metadata/mod.rs | 33 +++--- 4 files changed, 79 insertions(+), 88 deletions(-) diff --git a/rust/cbork-utils/src/decode_helper.rs b/rust/cbork-utils/src/decode_helper.rs index 1a8ab480f1..3ff57a532c 100644 --- a/rust/cbork-utils/src/decode_helper.rs +++ b/rust/cbork-utils/src/decode_helper.rs @@ -75,6 +75,9 @@ pub fn decode_tag(d: &mut Decoder, from: &str) -> Result { } /// Decode any in CDDL (any CBOR type) and return its bytes. +/// This function **allows** unused remainder bytes, unlike [`decode_any_to_end`]. +/// Unless an element of the [RFC 8742 CBOR Sequence](https://datatracker.ietf.org/doc/rfc8742/) +/// is expected to be decoded, the use of this function might cause invalid input to pass. /// /// # Errors /// @@ -92,6 +95,24 @@ pub fn decode_any<'d>(d: &mut Decoder<'d>, from: &str) -> Result<&'d [u8], decod Ok(bytes) } +/// Decode any in CDDL (any CBOR type) and return its bytes. This function guarantees that +/// no unused bytes remain in the [`Decoder`]. If unused remainder is expected, use +/// [`decode_any`]. +/// +/// # Errors +/// +/// Error if the decoding fails or if [`Decoder`] is not fully consumed. +pub fn decode_any_to_end<'d>(d: &mut Decoder<'d>, from: &str) -> Result<&'d [u8], decode::Error> { + let decoded = decode_any(d, from)?; + if d.position() == d.input().len() { + Ok(decoded) + } else { + Err(decode::Error::message(format!( + "Unused bytes remain in the input after decoding in {from}" + ))) + } +} + #[cfg(test)] mod tests { use minicbor::Encoder; @@ -177,4 +198,12 @@ mod tests { // Should print out the error message with the location of the error assert!(result.is_err()); } + + #[test] + fn test_decode_any_seq() { + let mut d = Decoder::new(&[]); + let result = decode_any(&mut d, "test"); + // Should print out the error message with the location of the error + assert!(result.is_err()); + } } diff --git a/rust/signed_doc/Cargo.toml b/rust/signed_doc/Cargo.toml index 62c4c6b8f7..6f9426efd5 100644 --- a/rust/signed_doc/Cargo.toml +++ b/rust/signed_doc/Cargo.toml @@ -12,12 +12,13 @@ workspace = true [dependencies] catalyst-types = { version = "0.0.3", path = "../catalyst-types" } +cbork-utils = { version = "0.0.1", path = "../cbork-utils" } anyhow = "1.0.95" serde = { version = "1.0.217", features = ["derive"] } -serde_json = "1.0.134" +serde_json = { version = "1.0.134", features = ["raw_value"] } coset = "0.3.8" -minicbor = { version = "0.25.1", features = ["half"] } +minicbor = "0.25.1" brotli = "7.0.0" ed25519-dalek = { version = "2.1.1", features = ["rand_core", "pem"] } hex = "0.4.3" diff --git a/rust/signed_doc/src/metadata/content_type.rs b/rust/signed_doc/src/metadata/content_type.rs index b72cb4b9c2..687f246974 100644 --- a/rust/signed_doc/src/metadata/content_type.rs +++ b/rust/signed_doc/src/metadata/content_type.rs @@ -1,20 +1,17 @@ //! Document Payload Content Type. -use std::{ - fmt::{Display, Formatter}, - str::FromStr, -}; - -use coset::iana::CoapContentFormat; -use serde::{de, Deserialize, Deserializer}; -use strum::VariantArray; +use cbork_utils::decode_helper::decode_any_to_end; +use strum::{AsRefStr, Display as EnumDisplay, EnumString, VariantArray}; /// Payload Content Type. -#[derive(Debug, Copy, Clone, PartialEq, Eq, VariantArray)] +// TODO: add custom parse error type when the [strum issue]([`issue`](https://github.com/Peternator7/strum/issues/430)) fix is merged. +#[derive(Debug, Copy, Clone, PartialEq, Eq, VariantArray, EnumString, EnumDisplay, AsRefStr)] pub enum ContentType { /// 'application/cbor' + #[strum(to_string = "application/cbor")] Cbor, /// 'application/json' + #[strum(to_string = "application/json")] Json, } @@ -23,90 +20,53 @@ impl ContentType { pub(crate) fn validate(self, content: &[u8]) -> anyhow::Result<()> { match self { Self::Json => { - if let Err(e) = serde_json::from_slice::(content) { + if let Err(e) = serde_json::from_slice::<&serde_json::value::RawValue>(content) { anyhow::bail!("Invalid {self} content: {e}") } }, Self::Cbor => { - if let Err(e) = minicbor::decode::(content) { + if let Err(e) = + decode_any_to_end(&mut minicbor::Decoder::new(content), "signed doc content") + { anyhow::bail!("Invalid {self} content: {e}") } }, } Ok(()) } -} -impl Display for ContentType { - fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { - match self { - Self::Cbor => write!(f, "application/cbor"), - Self::Json => write!(f, "application/json"), - } + fn decode_error(input: &str) -> minicbor::decode::Error { + minicbor::decode::Error::message(format!( + "Unsupported Content Type {input:?}, Supported only: {:?}", + ContentType::VARIANTS + .iter() + .map(AsRef::as_ref) + .collect::>() + )) } } -impl FromStr for ContentType { - type Err = anyhow::Error; - - fn from_str(s: &str) -> Result { - match s { - "application/cbor" => Ok(Self::Cbor), - "application/json" => Ok(Self::Json), - _ => { - anyhow::bail!( - "Unsupported Content Type: {s:?}, Supported only: {:?}", - ContentType::VARIANTS - .iter() - .map(ToString::to_string) - .collect::>() - ) - }, - } - } -} - -impl<'de> Deserialize<'de> for ContentType { - fn deserialize(deserializer: D) -> Result - where D: Deserializer<'de> { - let s = String::deserialize(deserializer)?; - FromStr::from_str(&s).map_err(de::Error::custom) - } -} - -impl From for CoapContentFormat { - fn from(value: ContentType) -> Self { - match value { - ContentType::Cbor => Self::Cbor, - ContentType::Json => Self::Json, - } +impl minicbor::Encode for ContentType { + fn encode( + &self, e: &mut minicbor::Encoder, _: &mut C, + ) -> Result<(), minicbor::encode::Error> { + e.str(self.as_ref())?; + Ok(()) } } -impl TryFrom<&coset::ContentType> for ContentType { - type Error = anyhow::Error; - - fn try_from(value: &coset::ContentType) -> Result { - let content_type = match value { - coset::ContentType::Assigned(CoapContentFormat::Json) => ContentType::Json, - coset::ContentType::Assigned(CoapContentFormat::Cbor) => ContentType::Cbor, - _ => { - anyhow::bail!( - "Unsupported Content Type {value:?}, Supported only: {:?}", - ContentType::VARIANTS - .iter() - .map(ToString::to_string) - .collect::>() - ) - }, - }; - Ok(content_type) +impl<'b, C> minicbor::Decode<'b, C> for ContentType { + fn decode(d: &mut minicbor::Decoder<'b>, _: &mut C) -> Result { + let s = d.str()?; + let decoded = s.parse().map_err(|_| Self::decode_error(s))?; + Ok(decoded) } } #[cfg(test)] mod tests { use super::*; + use std::str::FromStr as _; #[test] fn content_type_validate_test() { diff --git a/rust/signed_doc/src/metadata/mod.rs b/rust/signed_doc/src/metadata/mod.rs index bbbdb1677d..414128ca3a 100644 --- a/rust/signed_doc/src/metadata/mod.rs +++ b/rust/signed_doc/src/metadata/mod.rs @@ -14,7 +14,7 @@ use catalyst_types::{ }; pub use content_encoding::ContentEncoding; pub use content_type::ContentType; -use coset::{cbor::Value, iana::CoapContentFormat}; +use coset::cbor::Value; pub use document_ref::DocumentRef; pub use extra_fields::ExtraFields; pub use section::Section; @@ -154,19 +154,19 @@ impl InnerMetadata { ..Self::default() }; - if let Some(value) = protected.header.content_type.as_ref() { - match ContentType::try_from(value) { - Ok(ct) => metadata.content_type = Some(ct), - Err(e) => { - report.conversion_error( - "COSE protected header content type", - &format!("{value:?}"), - &format!("Expected ContentType: {e}"), - &format!("{COSE_DECODING_CONTEXT}, ContentType"), - ); - }, - } - } + // if let Some(value) = protected.header.content_type.as_ref() { + // match ContentType::try_from(value) { + // Ok(ct) => metadata.content_type = Some(ct), + // Err(e) => { + // 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, @@ -230,8 +230,9 @@ impl TryFrom<&Metadata> for coset::Header { type Error = anyhow::Error; fn try_from(meta: &Metadata) -> Result { - let mut builder = coset::HeaderBuilder::new() - .content_format(CoapContentFormat::from(meta.content_type()?)); + // let mut builder = coset::HeaderBuilder::new() + // .content_format(CoapContentFormat::from(meta.content_type()?)); + let mut builder = coset::HeaderBuilder::new(); if let Some(content_encoding) = meta.content_encoding() { builder = builder.text_value( From 88f75d7f791a0b1ae537d1e99a2239e6bb06d2b0 Mon Sep 17 00:00:00 2001 From: no30bit Date: Fri, 23 May 2025 18:02:02 +0300 Subject: [PATCH 02/16] fix docs --- rust/signed_doc/src/metadata/content_type.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rust/signed_doc/src/metadata/content_type.rs b/rust/signed_doc/src/metadata/content_type.rs index 687f246974..9ca7a69efd 100644 --- a/rust/signed_doc/src/metadata/content_type.rs +++ b/rust/signed_doc/src/metadata/content_type.rs @@ -35,6 +35,7 @@ impl ContentType { Ok(()) } + /// An error returned on [`minicbor::Decode::decode`] failure. fn decode_error(input: &str) -> minicbor::decode::Error { minicbor::decode::Error::message(format!( "Unsupported Content Type {input:?}, Supported only: {:?}", @@ -65,9 +66,10 @@ impl<'b, C> minicbor::Decode<'b, C> for ContentType { #[cfg(test)] mod tests { - use super::*; use std::str::FromStr as _; + use super::*; + #[test] fn content_type_validate_test() { let json_bytes = serde_json::to_vec(&serde_json::Value::Null).unwrap(); From 617a07f673b88868b15a197d8218384b352d0f13 Mon Sep 17 00:00:00 2001 From: no30bit Date: Tue, 27 May 2025 12:41:47 +0300 Subject: [PATCH 03/16] enc/dec for doc ref --- rust/cbork-utils/src/decode_helper.rs | 26 +++++++- .../src/metadata/content_encoding.rs | 63 +++++++------------ rust/signed_doc/src/metadata/document_ref.rs | 27 ++++++++ 3 files changed, 76 insertions(+), 40 deletions(-) diff --git a/rust/cbork-utils/src/decode_helper.rs b/rust/cbork-utils/src/decode_helper.rs index 3ff57a532c..cc052fbf50 100644 --- a/rust/cbork-utils/src/decode_helper.rs +++ b/rust/cbork-utils/src/decode_helper.rs @@ -10,7 +10,9 @@ use minicbor::{data::Tag, decode, Decoder}; pub fn decode_helper<'a, T, C>( d: &mut Decoder<'a>, from: &str, context: &mut C, ) -> Result -where T: minicbor::Decode<'a, C> { +where + T: minicbor::Decode<'a, C>, +{ T::decode(d, context).map_err(|e| { decode::Error::message(format!( "Failed to decode {:?} in {from}: {e}", @@ -19,6 +21,28 @@ where T: minicbor::Decode<'a, C> { }) } +/// Generic helper function for decoding different types. +/// +/// # Errors +/// +/// Error if the decoding fails. +pub fn decode_to_end_helper<'a, T, C>( + d: &mut Decoder<'a>, from: &str, context: &mut C, +) -> Result +where + T: minicbor::Decode<'a, C>, +{ + let decoded = decode_helper(d, from, context)?; + if d.position() == d.input().len() { + Ok(decoded) + } else { + Err(decode::Error::message(format!( + "Unused bytes remain in the input after decoding {:?} in {from}", + std::any::type_name::() + ))) + } +} + /// Helper function for decoding bytes. /// /// # Errors diff --git a/rust/signed_doc/src/metadata/content_encoding.rs b/rust/signed_doc/src/metadata/content_encoding.rs index d47f696e7f..8e9843779a 100644 --- a/rust/signed_doc/src/metadata/content_encoding.rs +++ b/rust/signed_doc/src/metadata/content_encoding.rs @@ -1,16 +1,13 @@ //! Document Payload Content Encoding. -use std::{ - fmt::{Display, Formatter}, - str::FromStr, -}; - -use serde::{de, Deserialize, Deserializer}; +use strum::{AsRefStr, Display as EnumDisplay, EnumString, VariantArray}; /// IANA `CoAP` Content Encoding. -#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, VariantArray, EnumDisplay, EnumString, AsRefStr)] +// TODO: add custom parse error type when the [strum issue]([`issue`](https://github.com/Peternator7/strum/issues/430)) fix is merged. pub enum ContentEncoding { /// Brotli compression.format. + #[strum(to_string = "br")] Brotli, } @@ -43,44 +40,32 @@ impl ContentEncoding { }, } } -} - -impl Display for ContentEncoding { - fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { - match self { - Self::Brotli => write!(f, "br"), - } - } -} - -impl FromStr for ContentEncoding { - type Err = anyhow::Error; - fn from_str(encoding: &str) -> Result { - match encoding { - "br" => Ok(ContentEncoding::Brotli), - _ => anyhow::bail!("Unsupported Content Encoding: {encoding:?}"), - } + /// An error returned on [`minicbor::Decode::decode`] failure. + fn decode_error(input: &str) -> minicbor::decode::Error { + minicbor::decode::Error::message(format!( + "Unsupported Content Type {input:?}, Supported only: {:?}", + ContentEncoding::VARIANTS + .iter() + .map(AsRef::as_ref) + .collect::>() + )) } } -impl<'de> Deserialize<'de> for ContentEncoding { - fn deserialize(deserializer: D) -> Result - where D: Deserializer<'de> { - let s = String::deserialize(deserializer)?; - FromStr::from_str(&s).map_err(de::Error::custom) +impl minicbor::Encode for ContentEncoding { + fn encode( + &self, e: &mut minicbor::Encoder, _: &mut C, + ) -> Result<(), minicbor::encode::Error> { + e.str(self.as_ref())?; + Ok(()) } } -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<'b, C> minicbor::Decode<'b, C> for ContentEncoding { + fn decode(d: &mut minicbor::Decoder<'b>, _: &mut C) -> Result { + let s = d.str()?; + let decoded = s.parse().map_err(|_| Self::decode_error(s))?; + Ok(decoded) } } diff --git a/rust/signed_doc/src/metadata/document_ref.rs b/rust/signed_doc/src/metadata/document_ref.rs index 00e0bba241..a3a9bd7dbb 100644 --- a/rust/signed_doc/src/metadata/document_ref.rs +++ b/rust/signed_doc/src/metadata/document_ref.rs @@ -2,6 +2,8 @@ use std::fmt::Display; +use catalyst_types::uuid::CborContext; +use cbork_utils::decode_helper; use coset::cbor::Value; use super::{utils::CborUuidV7, UuidV7}; @@ -21,6 +23,31 @@ impl Display for DocumentRef { } } +impl minicbor::Encode for DocumentRef { + fn encode( + &self, e: &mut minicbor::Encoder, ctx: &mut CborContext, + ) -> Result<(), minicbor::encode::Error> { + [self.id, self.ver].encode(e, ctx) + } +} + +impl minicbor::Decode<'_, CborContext> for DocumentRef { + fn decode( + d: &mut minicbor::Decoder, ctx: &mut CborContext, + ) -> Result { + let (id, ver): (UuidV7, UuidV7) = + decode_helper::decode_to_end_helper(d, "document reference", ctx).map_err(|err| { + err.with_message("Document Reference array of two UUIDs was expected") + })?; + if ver < id { + return Err(minicbor::decode::Error::message( + "Document Reference Version can never be smaller than its ID", + )); + } + Ok(Self { id, ver }) + } +} + impl TryFrom for Value { type Error = anyhow::Error; From 1192b9038bb62dfc525488e9e1eb5f2e00294fb7 Mon Sep 17 00:00:00 2001 From: no30bit Date: Wed, 28 May 2025 02:28:31 +0300 Subject: [PATCH 04/16] protected header & builder --- rust/cbork-utils/src/decode_helper.rs | 8 +-- rust/signed_doc/Cargo.toml | 2 +- rust/signed_doc/src/builder.rs | 62 +++++++++++-------- rust/signed_doc/src/builder/cbor_map.rs | 49 +++++++++++++++ rust/signed_doc/src/builder/cose_sign.rs | 1 + .../src/builder/protected_header.rs | 59 ++++++++++++++++++ rust/signed_doc/src/lib.rs | 4 +- .../src/metadata/content_encoding.rs | 6 +- rust/signed_doc/src/metadata/content_type.rs | 6 +- rust/signed_doc/src/metadata/document_ref.rs | 38 +----------- rust/signed_doc/src/metadata/extra_fields.rs | 2 +- rust/signed_doc/src/metadata/mod.rs | 30 ++++----- rust/signed_doc/src/metadata/section.rs | 45 ++++---------- 13 files changed, 185 insertions(+), 127 deletions(-) create mode 100644 rust/signed_doc/src/builder/cbor_map.rs create mode 100644 rust/signed_doc/src/builder/cose_sign.rs create mode 100644 rust/signed_doc/src/builder/protected_header.rs diff --git a/rust/cbork-utils/src/decode_helper.rs b/rust/cbork-utils/src/decode_helper.rs index cc052fbf50..88d87f974c 100644 --- a/rust/cbork-utils/src/decode_helper.rs +++ b/rust/cbork-utils/src/decode_helper.rs @@ -10,9 +10,7 @@ use minicbor::{data::Tag, decode, Decoder}; pub fn decode_helper<'a, T, C>( d: &mut Decoder<'a>, from: &str, context: &mut C, ) -> Result -where - T: minicbor::Decode<'a, C>, -{ +where T: minicbor::Decode<'a, C> { T::decode(d, context).map_err(|e| { decode::Error::message(format!( "Failed to decode {:?} in {from}: {e}", @@ -29,9 +27,7 @@ where pub fn decode_to_end_helper<'a, T, C>( d: &mut Decoder<'a>, from: &str, context: &mut C, ) -> Result -where - T: minicbor::Decode<'a, C>, -{ +where T: minicbor::Decode<'a, C> { let decoded = decode_helper(d, from, context)?; if d.position() == d.input().len() { Ok(decoded) diff --git a/rust/signed_doc/Cargo.toml b/rust/signed_doc/Cargo.toml index 6f9426efd5..71a34b780e 100644 --- a/rust/signed_doc/Cargo.toml +++ b/rust/signed_doc/Cargo.toml @@ -18,7 +18,7 @@ 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 = "0.25.1" +minicbor = { version = "0.25.1", features = ["std"] } brotli = "7.0.0" ed25519-dalek = { version = "2.1.1", features = ["rand_core", "pem"] } hex = "0.4.3" diff --git a/rust/signed_doc/src/builder.rs b/rust/signed_doc/src/builder.rs index 8a2c6c925a..9b87453e30 100644 --- a/rust/signed_doc/src/builder.rs +++ b/rust/signed_doc/src/builder.rs @@ -1,49 +1,61 @@ //! Catalyst Signed Document Builder. + +/// An implementation of [`CborMap`]. +mod cbor_map; +/// Signed payload with signatures as described in +/// [section 2 of RFC 8152](https://datatracker.ietf.org/doc/html/rfc8152#section-2). +/// Specialized for Catalyst Signed Document (e.g. no support for unprotected headers). +mod cose_sign; +/// COSE protected header as per [RFC 8152](https://datatracker.ietf.org/doc/html/rfc8152#autoid-8), +/// but with some fields omitted when unused by Catalyst and some fields specialized for +/// it. +mod protected_header; + +use std::convert::Infallible; + use catalyst_types::{catalyst_id::CatalystId, problem_report::ProblemReport}; +use cbor_map::CborMap; +use minicbor::{bytes::ByteVec, data::Tag}; use crate::{ - signature::Signature, CatalystSignedDocument, Content, InnerCatalystSignedDocument, Metadata, - Signatures, PROBLEM_REPORT_CTX, + signature::Signature, CatalystSignedDocument, Content, ContentEncoding, + InnerCatalystSignedDocument, Metadata, PROBLEM_REPORT_CTX, }; -/// Catalyst Signed Document Builder. -#[derive(Debug)] -pub struct Builder(InnerCatalystSignedDocument); +pub type EncodeError = minicbor::encode::Error; -impl Default for Builder { - fn default() -> Self { - Self::new() - } +/// Catalyst Signed Document Builder. +#[derive(Debug, Default)] +pub struct Builder { + metadata: CborMap, + content: Option, + signatures: Vec<(CatalystId, ByteVec)>, } impl Builder { /// Start building a signed document #[must_use] pub fn new() -> Self { - let report = ProblemReport::new(PROBLEM_REPORT_CTX); - Self(InnerCatalystSignedDocument { - report, - metadata: Metadata::default(), - content: Content::default(), - signatures: Signatures::default(), - }) + Self::default() } - /// Set document metadata in JSON format - /// Collect problem report if some fields are missing. + /// Set document field metadata. /// /// # Errors - /// - Fails if it is invalid metadata fields JSON object. - pub fn with_json_metadata(mut self, json: serde_json::Value) -> anyhow::Result { - let metadata = serde_json::from_value(json)?; - self.0.metadata = Metadata::from_metadata_fields(metadata, &self.0.report); + /// - Fails if it the CBOR encoding fails. + pub fn add_metadata_field, V: minicbor::Encode>( + mut self, ctx: &mut C, key: K, v: V, + ) -> Result { + // Ignoring pre-insert existence of the key. + let _: Option<_> = self.metadata.encode_and_insert(ctx, key, v)?; Ok(self) } - /// Set decoded (original) document content bytes + /// Set document content bytes (if content is encoded, it should be aligned with the + /// encoding algorithm from the `content-encoding` field. #[must_use] - pub fn with_decoded_content(mut self, content: Vec) -> Self { - self.0.content = Content::from_decoded(content); + pub fn with_content(mut self, content: Vec) -> Self { + self.content = Some(content.into()); self } diff --git a/rust/signed_doc/src/builder/cbor_map.rs b/rust/signed_doc/src/builder/cbor_map.rs new file mode 100644 index 0000000000..1ea73f7fce --- /dev/null +++ b/rust/signed_doc/src/builder/cbor_map.rs @@ -0,0 +1,49 @@ +use std::collections::BTreeMap; + +use super::EncodeError; + +/// A map of CBOR encoded key-value pairs with **bytewise** lexicographic key ordering. +#[derive(Debug, Default)] +pub struct CborMap(BTreeMap, Vec>); + +impl CborMap { + /// Creates an empty map. + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// A number of entries in a map. + pub fn len(&self) -> usize { + self.0.len() + } + + /// Is there no entries in the map. + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Encodes a key-value pair to CBOR and then inserts it into the map. + /// + /// If the map did not have this key present, [`None`] is returned. + /// + /// If the map did have this key present, the value is updated, and the old + /// CBOR-encoded value is returned. + pub fn encode_and_insert, V: minicbor::Encode>( + &mut self, ctx: &mut C, key: K, v: V, + ) -> Result>, EncodeError> { + let (encoded_key, encoded_v) = ( + minicbor::to_vec_with(key, ctx)?, + minicbor::to_vec_with(v, ctx)?, + ); + Ok(self.0.insert(encoded_key, encoded_v)) + } + + /// Iterate over CBOR-encoded key-value pairs. + /// Items are returned in **bytewise** lexicographic key ordering. + pub fn iter(&self) -> impl Iterator { + self.0 + .iter() + .map(|(key_vec, value_vec)| (key_vec.as_slice(), value_vec.as_slice())) + } +} diff --git a/rust/signed_doc/src/builder/cose_sign.rs b/rust/signed_doc/src/builder/cose_sign.rs new file mode 100644 index 0000000000..e3282bd286 --- /dev/null +++ b/rust/signed_doc/src/builder/cose_sign.rs @@ -0,0 +1 @@ +pub fn make_cose_sign() {} diff --git a/rust/signed_doc/src/builder/protected_header.rs b/rust/signed_doc/src/builder/protected_header.rs new file mode 100644 index 0000000000..16d45a2e37 --- /dev/null +++ b/rust/signed_doc/src/builder/protected_header.rs @@ -0,0 +1,59 @@ +use minicbor::{ + bytes::{ByteSlice, ByteVec}, + Encode as _, +}; + +use super::{cbor_map::CborMap, EncodeError}; + +/// The KID label as per [RFC 8152 3.1 section](https://datatracker.ietf.org/doc/html/rfc8152#section-3.1). +pub const KID_LABEL: u8 = 4; + +/// Since [`KID_LABEL`] fits into `0 ..= 0x17`, it is encoded as is. +const fn kid_label_bytes() -> &'static [u8] { + &[KID_LABEL] +} + +/// Make to-be-signed data using the provided cbor-encoded key-value pairs representing +/// fields, conforming to the protected fields specification. +/// +/// The validity of the encoding of the fields argument is not checked. +/// +/// `kid` field is encoded in a map, respecting **bytewise** lexicographic key ordering. +/// +/// `kid` is encoded as a byte string as is. +/// +/// # Errors +/// +/// - If encoding of the `kid` fails. +/// - If `kid` field is found in the metadata fields. +pub fn make_protected_header( + kid: &ByteSlice, metadata_fields: &CborMap, +) -> Result { + let mut encoder = minicbor::Encoder::new(vec![]); + let mut fields_iter = metadata_fields.iter(); + // Incrementing to include `kid` entry. + let map_len = u64::try_from(metadata_fields.len().saturating_add(1)).unwrap_or(u64::MAX); + encoder.map(map_len); + // Peeking through the metadata fields to insert `kid` in order. + loop { + let next_field = fields_iter.next(); + if next_field.is_some_and(|(encoded_key, _)| encoded_key == kid_label_bytes()) { + return Err(EncodeError::message( + "kid label found in the metadata fields", + )); + } + // Writing `kid` where it would have been with *bytewise** lexicographic order. + if next_field.is_none_or(|(encoded_key, _)| encoded_key > kid_label_bytes()) { + KID_LABEL.encode(&mut encoder, &mut ())?; + kid.encode(&mut encoder, &mut ())?; + } + let Some((encoded_key, encoded_v)) = next_field else { + break; + }; + // Writing a pre-encoded field of the map. + encoder.writer_mut().extend_from_slice(encoded_key); + encoder.writer_mut().extend_from_slice(encoded_v); + } + + Ok(ByteVec::from(encoder.into_writer())) +} diff --git a/rust/signed_doc/src/lib.rs b/rust/signed_doc/src/lib.rs index 39908e7f11..f0add61009 100644 --- a/rust/signed_doc/src/lib.rs +++ b/rust/signed_doc/src/lib.rs @@ -190,11 +190,11 @@ impl InnerCatalystSignedDocument { let protected_header = Header::try_from(&self.metadata).context("Failed to encode Document Metadata")?; - let content = self + let content = sself .content .encoded_bytes(self.metadata.content_encoding())?; - let mut builder = coset::CoseSignBuilder::new() + let mut builder = coet::CoseSignBuilder::new() .protected(protected_header) .payload(content); diff --git a/rust/signed_doc/src/metadata/content_encoding.rs b/rust/signed_doc/src/metadata/content_encoding.rs index 8e9843779a..2c6d194fdd 100644 --- a/rust/signed_doc/src/metadata/content_encoding.rs +++ b/rust/signed_doc/src/metadata/content_encoding.rs @@ -57,15 +57,13 @@ impl minicbor::Encode for ContentEncoding { fn encode( &self, e: &mut minicbor::Encoder, _: &mut C, ) -> Result<(), minicbor::encode::Error> { - e.str(self.as_ref())?; - Ok(()) + e.str(self.as_ref())?.ok() } } impl<'b, C> minicbor::Decode<'b, C> for ContentEncoding { fn decode(d: &mut minicbor::Decoder<'b>, _: &mut C) -> Result { let s = d.str()?; - let decoded = s.parse().map_err(|_| Self::decode_error(s))?; - Ok(decoded) + s.parse().map_err(|_| Self::decode_error(s)) } } diff --git a/rust/signed_doc/src/metadata/content_type.rs b/rust/signed_doc/src/metadata/content_type.rs index 9ca7a69efd..67f72425c4 100644 --- a/rust/signed_doc/src/metadata/content_type.rs +++ b/rust/signed_doc/src/metadata/content_type.rs @@ -51,16 +51,14 @@ impl minicbor::Encode for ContentType { fn encode( &self, e: &mut minicbor::Encoder, _: &mut C, ) -> Result<(), minicbor::encode::Error> { - e.str(self.as_ref())?; - Ok(()) + e.str(self.as_ref())?.ok() } } impl<'b, C> minicbor::Decode<'b, C> for ContentType { fn decode(d: &mut minicbor::Decoder<'b>, _: &mut C) -> Result { let s = d.str()?; - let decoded = s.parse().map_err(|_| Self::decode_error(s))?; - Ok(decoded) + s.parse().map_err(|_| Self::decode_error(s)) } } diff --git a/rust/signed_doc/src/metadata/document_ref.rs b/rust/signed_doc/src/metadata/document_ref.rs index a3a9bd7dbb..57d6e3affe 100644 --- a/rust/signed_doc/src/metadata/document_ref.rs +++ b/rust/signed_doc/src/metadata/document_ref.rs @@ -4,9 +4,8 @@ use std::fmt::Display; use catalyst_types::uuid::CborContext; use cbork_utils::decode_helper; -use coset::cbor::Value; -use super::{utils::CborUuidV7, UuidV7}; +use super::UuidV7; /// Reference to a Document. #[derive(Copy, Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] @@ -36,7 +35,7 @@ impl minicbor::Decode<'_, CborContext> for DocumentRef { d: &mut minicbor::Decoder, ctx: &mut CborContext, ) -> Result { let (id, ver): (UuidV7, UuidV7) = - decode_helper::decode_to_end_helper(d, "document reference", ctx).map_err(|err| { + decode_helper::decode_helper(d, "document reference", ctx).map_err(|err| { err.with_message("Document Reference array of two UUIDs was expected") })?; if ver < id { @@ -47,36 +46,3 @@ impl minicbor::Decode<'_, CborContext> for DocumentRef { Ok(Self { id, ver }) } } - -impl TryFrom for Value { - type Error = anyhow::Error; - - fn try_from(value: DocumentRef) -> Result { - Ok(Value::Array(vec![ - Value::try_from(CborUuidV7(value.id))?, - Value::try_from(CborUuidV7(value.ver))?, - ])) - } -} - -impl TryFrom<&Value> for DocumentRef { - type Error = anyhow::Error; - - #[allow(clippy::indexing_slicing)] - fn try_from(val: &Value) -> anyhow::Result { - let Some(array) = val.as_array() else { - anyhow::bail!("Document Reference must be either a single UUID or an array of two"); - }; - anyhow::ensure!( - array.len() == 2, - "Document Reference array of two UUIDs was expected" - ); - let CborUuidV7(id) = CborUuidV7::try_from(&array[0])?; - let CborUuidV7(ver) = CborUuidV7::try_from(&array[1])?; - anyhow::ensure!( - ver >= id, - "Document Reference Version can never be smaller than its ID" - ); - Ok(DocumentRef { id, ver }) - } -} diff --git a/rust/signed_doc/src/metadata/extra_fields.rs b/rust/signed_doc/src/metadata/extra_fields.rs index 5decc4a784..ad102fd075 100644 --- a/rust/signed_doc/src/metadata/extra_fields.rs +++ b/rust/signed_doc/src/metadata/extra_fields.rs @@ -30,7 +30,7 @@ const CATEGORY_ID_KEY: &str = "category_id"; /// Extra Metadata Fields. /// /// These values are extracted from the COSE Sign protected header labels. -#[derive(Clone, Default, Debug, PartialEq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Default, Debug, PartialEq)] pub struct ExtraFields { /// Reference to the latest document. #[serde(rename = "ref", skip_serializing_if = "Option::is_none")] diff --git a/rust/signed_doc/src/metadata/mod.rs b/rust/signed_doc/src/metadata/mod.rs index 414128ca3a..192680469c 100644 --- a/rust/signed_doc/src/metadata/mod.rs +++ b/rust/signed_doc/src/metadata/mod.rs @@ -154,19 +154,19 @@ impl InnerMetadata { ..Self::default() }; - // if let Some(value) = protected.header.content_type.as_ref() { - // match ContentType::try_from(value) { - // Ok(ct) => metadata.content_type = Some(ct), - // Err(e) => { - // report.conversion_error( - // "COSE protected header content type", - // &format!("{value:?}"), - // &format!("Expected ContentType: {e}"), - // &format!("{COSE_DECODING_CONTEXT}, ContentType"), - // ); - // }, - // } - // } + if let Some(value) = protected.header.content_type.as_ref() { + match ContentType::try_from(value) { + Ok(ct) => metadata.content_type = Some(ct), + Err(e) => { + 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, @@ -230,8 +230,8 @@ impl TryFrom<&Metadata> for coset::Header { type Error = anyhow::Error; fn try_from(meta: &Metadata) -> Result { - // let mut builder = coset::HeaderBuilder::new() - // .content_format(CoapContentFormat::from(meta.content_type()?)); + let mut builder = coset::HeaderBuilder::new() + .content_format(CoapContentFormat::from(meta.content_type()?)); let mut builder = coset::HeaderBuilder::new(); if let Some(content_encoding) = meta.content_encoding() { diff --git a/rust/signed_doc/src/metadata/section.rs b/rust/signed_doc/src/metadata/section.rs index 01e6a02a1b..78a313489c 100644 --- a/rust/signed_doc/src/metadata/section.rs +++ b/rust/signed_doc/src/metadata/section.rs @@ -2,9 +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 #[derive(Clone, Debug, PartialEq)] pub struct Section(jsonpath_rust::JsonPath); @@ -15,44 +12,26 @@ impl Display for Section { } } -impl Serialize for Section { - fn serialize(&self, serializer: S) -> Result - where S: serde::Serializer { - self.to_string().serialize(serializer) - } -} - -impl<'de> Deserialize<'de> for Section { - fn deserialize(deserializer: D) -> Result - where D: serde::Deserializer<'de> { - let str = String::deserialize(deserializer)?; - Self::from_str(&str).map_err(serde::de::Error::custom) - } -} - impl FromStr for Section { - type Err = anyhow::Error; + type Err = jsonpath_rust::JsonPathParserError; fn from_str(s: &str) -> Result { - Ok(Self( - jsonpath_rust::JsonPath::::from_str(s)?, - )) + jsonpath_rust::JsonPath::::from_str(s).map(Self) } } -impl From
for Value { - fn from(value: Section) -> Self { - Value::Text(value.to_string()) +impl minicbor::Encode for Section { + fn encode( + &self, e: &mut minicbor::Encoder, _: &mut C, + ) -> Result<(), minicbor::encode::Error> { + e.str(&self.0.to_string())?; + Ok(()) } } -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<'b, C> minicbor::Decode<'b, C> for Section { + fn decode(d: &mut minicbor::Decoder<'b>, _: &mut C) -> Result { + let s = d.str()?; + s.parse().map_err(minicbor::decode::Error::custom) } } From 9a5e9d0807595050aec10abb1cb339fbd9ae1cb2 Mon Sep 17 00:00:00 2001 From: no30bit Date: Wed, 28 May 2025 04:50:40 +0300 Subject: [PATCH 05/16] adding signatures --- rust/signed_doc/src/builder.rs | 82 ++++++++----------- rust/signed_doc/src/builder/cose_header.rs | 37 +++++++++ rust/signed_doc/src/builder/cose_sign.rs | 62 +++++++++++++- .../src/builder/protected_header.rs | 59 ------------- rust/signed_doc/src/lib.rs | 4 +- 5 files changed, 132 insertions(+), 112 deletions(-) create mode 100644 rust/signed_doc/src/builder/cose_header.rs delete mode 100644 rust/signed_doc/src/builder/protected_header.rs diff --git a/rust/signed_doc/src/builder.rs b/rust/signed_doc/src/builder.rs index 9b87453e30..9b5f85c3f8 100644 --- a/rust/signed_doc/src/builder.rs +++ b/rust/signed_doc/src/builder.rs @@ -2,41 +2,46 @@ /// An implementation of [`CborMap`]. mod cbor_map; +/// COSE protected header as per [RFC 8152](https://datatracker.ietf.org/doc/html/rfc8152#autoid-8), +/// but with some fields omitted when unused by Catalyst and some fields specialized for +/// it. +mod cose_header; /// Signed payload with signatures as described in /// [section 2 of RFC 8152](https://datatracker.ietf.org/doc/html/rfc8152#section-2). /// Specialized for Catalyst Signed Document (e.g. no support for unprotected headers). mod cose_sign; -/// COSE protected header as per [RFC 8152](https://datatracker.ietf.org/doc/html/rfc8152#autoid-8), -/// but with some fields omitted when unused by Catalyst and some fields specialized for -/// it. -mod protected_header; use std::convert::Infallible; -use catalyst_types::{catalyst_id::CatalystId, problem_report::ProblemReport}; +use catalyst_types::catalyst_id::CatalystId; use cbor_map::CborMap; -use minicbor::{bytes::ByteVec, data::Tag}; +use cose_header::{make_metadata_header, make_signature_header}; +use cose_sign::make_tbs_data; -use crate::{ - signature::Signature, CatalystSignedDocument, Content, ContentEncoding, - InnerCatalystSignedDocument, Metadata, PROBLEM_REPORT_CTX, -}; +use crate::CatalystSignedDocument; pub type EncodeError = minicbor::encode::Error; /// Catalyst Signed Document Builder. -#[derive(Debug, Default)] +#[derive(Debug)] pub struct Builder { metadata: CborMap, - content: Option, - signatures: Vec<(CatalystId, ByteVec)>, + content: Vec, + signatures: Vec<(CatalystId, Vec)>, } impl Builder { - /// Start building a signed document + /// Start building a signed document. + /// + /// Sets document content bytes. If content is encoded, it should be aligned with the + /// encoding algorithm from the `content-encoding` field. #[must_use] - pub fn new() -> Self { - Self::default() + pub fn from_content(content: Vec) -> Self { + Self { + metadata: CborMap::default(), + content: content.into(), + signatures: vec![], + } } /// Set document field metadata. @@ -51,14 +56,6 @@ impl Builder { Ok(self) } - /// Set document content bytes (if content is encoded, it should be aligned with the - /// encoding algorithm from the `content-encoding` field. - #[must_use] - pub fn with_content(mut self, content: Vec) -> Self { - self.content = Some(content.into()); - self - } - /// Add a signature to the document /// /// # Errors @@ -67,23 +64,19 @@ impl Builder { /// content, due to malformed data, or when the signed document cannot be /// converted into `coset::CoseSign`. pub fn add_signature( - mut self, sign_fn: impl FnOnce(Vec) -> Vec, kid: &CatalystId, - ) -> anyhow::Result { - let cose_sign = self - .0 - .as_cose_sign() - .map_err(|e| anyhow::anyhow!("Failed to sign: {e}"))?; + mut self, sign_fn: impl FnOnce(Vec) -> Vec, kid: CatalystId, + ) -> Result { + let metadata_header = make_metadata_header(&self.metadata); - let protected_header = coset::HeaderBuilder::new().key_id(kid.to_string().into_bytes()); + let kid_str = kid.to_string().into_bytes(); + let signature_header = make_signature_header(kid_str.as_slice())?; - let mut signature = coset::CoseSignatureBuilder::new() - .protected(protected_header.build()) - .build(); - let data_to_sign = cose_sign.tbs_data(&[], &signature); - signature.signature = sign_fn(data_to_sign); - if let Some(sign) = Signature::from_cose_sig(signature, &self.0.report) { - self.0.signatures.push(sign); - } + // Question: maybe this should be cached? + let tbs_data = make_tbs_data(&metadata_header, &signature_header, &self.content)?; + + let signature = sign_fn(tbs_data); + + self.signatures.push((kid, signature)); Ok(self) } @@ -95,14 +88,3 @@ impl Builder { self.0.into() } } - -impl From<&CatalystSignedDocument> for Builder { - fn from(value: &CatalystSignedDocument) -> Self { - Self(InnerCatalystSignedDocument { - metadata: value.inner.metadata.clone(), - content: value.inner.content.clone(), - signatures: value.inner.signatures.clone(), - report: value.inner.report.clone(), - }) - } -} diff --git a/rust/signed_doc/src/builder/cose_header.rs b/rust/signed_doc/src/builder/cose_header.rs new file mode 100644 index 0000000000..a4294d3651 --- /dev/null +++ b/rust/signed_doc/src/builder/cose_header.rs @@ -0,0 +1,37 @@ +use super::{cbor_map::CborMap, EncodeError}; + +/// The KID label as per [RFC 8152 3.1 section](https://datatracker.ietf.org/doc/html/rfc8152#section-3.1). +pub const KID_LABEL: u8 = 4; + +/// Since [`KID_LABEL`] fits into `0 ..= 0x17`, it is encoded as is. +const fn kid_label_bytes() -> &'static [u8] { + &[KID_LABEL] +} + +/// Make the header using the provided cbor-encoded key-value pairs representing +/// fields, conforming to the header fields specification. +pub fn make_metadata_header(metadata_fields: &CborMap) -> Vec { + let mut encoder = minicbor::Encoder::new(vec![]); + + let map_len = u64::try_from(metadata_fields.len()).unwrap_or(u64::MAX); + encoder.map(map_len); + for (encoded_key, encoded_v) in metadata_fields.iter() { + // Writing a pre-encoded field of the map. + encoder.writer_mut().extend_from_slice(encoded_key); + encoder.writer_mut().extend_from_slice(encoded_v); + } + + encoder.into_writer() +} + +/// Make the protected header for the `Cose_signature`, conforming to the header fields specification. +/// +/// # Errors +/// +/// - If encoding of the `kid` fails. +pub fn make_signature_header(kid: &[u8]) -> Result, EncodeError> { + let mut encoder = minicbor::Encoder::new(vec![]); + // A map with a single `kid` field. + encoder.map(1u64)?.u8(KID_LABEL)?.bytes(kid)?; + Ok(encoder.into_writer()) +} diff --git a/rust/signed_doc/src/builder/cose_sign.rs b/rust/signed_doc/src/builder/cose_sign.rs index e3282bd286..6658e6ac68 100644 --- a/rust/signed_doc/src/builder/cose_sign.rs +++ b/rust/signed_doc/src/builder/cose_sign.rs @@ -1 +1,61 @@ -pub fn make_cose_sign() {} +use minicbor::bytes::{ByteArray, ByteSlice}; + +use super::EncodeError; + +/// Make `Cose_signature`. +/// +/// Signature bytes should represent a cryptographic signature. +pub fn _make_cose_signature( + protected_header: &ByteSlice, signature_bytes: &ByteSlice, +) -> Result, EncodeError> { + minicbor::to_vec([protected_header, signature_bytes]) +} + +/// Collect an array from an iterator of pre-encoded `Cose_signature` items. +/// +/// Signature bytes should represent a cryptographic signature. +pub fn _collect_cose_signature_array(signatures: S) -> Result, EncodeError> +where + S: IntoIterator, IntoIter: ExactSizeIterator>, +{ + let iter = signatures.into_iter(); + let array_len = u64::try_from(iter.len().saturating_add(1)).unwrap_or(u64::MAX); + let mut encoder = minicbor::Encoder::new(vec![]); + encoder.array(array_len)?; + for signature in iter { + encoder.bytes(signature.as_ref())?; + } + Ok(encoder.into_writer()) +} + +/// Make cbor-encoded `Cose_Sign`. +pub fn _make_cose_sign( + protected_header: &ByteSlice, content: &ByteSlice, signatures: S, +) -> Result, EncodeError> +where + S: IntoIterator, IntoIter: ExactSizeIterator>, +{ + minicbor::to_vec(( + protected_header, + ByteArray::from([]), // unprotected. + content, + _collect_cose_signature_array(signatures)?, + )) +} + +/// Create a binary blob that will be signed and construct the to-be-signed data from it +/// in-place. +pub fn make_tbs_data( + metadata_header: &[u8], signature_header: &[u8], content: &[u8], +) -> Result, EncodeError> { + /// The context string as per [RFC 8152 section 4.4](https://datatracker.ietf.org/doc/html/rfc8152#section-4.4). + const SIGNATURE_CONTEXT: &str = "Signature"; + + minicbor::to_vec(( + SIGNATURE_CONTEXT, + <&ByteSlice>::from(metadata_header), + <&ByteSlice>::from(signature_header), + ByteArray::from([]), // aad. + <&ByteSlice>::from(content), + )) +} diff --git a/rust/signed_doc/src/builder/protected_header.rs b/rust/signed_doc/src/builder/protected_header.rs deleted file mode 100644 index 16d45a2e37..0000000000 --- a/rust/signed_doc/src/builder/protected_header.rs +++ /dev/null @@ -1,59 +0,0 @@ -use minicbor::{ - bytes::{ByteSlice, ByteVec}, - Encode as _, -}; - -use super::{cbor_map::CborMap, EncodeError}; - -/// The KID label as per [RFC 8152 3.1 section](https://datatracker.ietf.org/doc/html/rfc8152#section-3.1). -pub const KID_LABEL: u8 = 4; - -/// Since [`KID_LABEL`] fits into `0 ..= 0x17`, it is encoded as is. -const fn kid_label_bytes() -> &'static [u8] { - &[KID_LABEL] -} - -/// Make to-be-signed data using the provided cbor-encoded key-value pairs representing -/// fields, conforming to the protected fields specification. -/// -/// The validity of the encoding of the fields argument is not checked. -/// -/// `kid` field is encoded in a map, respecting **bytewise** lexicographic key ordering. -/// -/// `kid` is encoded as a byte string as is. -/// -/// # Errors -/// -/// - If encoding of the `kid` fails. -/// - If `kid` field is found in the metadata fields. -pub fn make_protected_header( - kid: &ByteSlice, metadata_fields: &CborMap, -) -> Result { - let mut encoder = minicbor::Encoder::new(vec![]); - let mut fields_iter = metadata_fields.iter(); - // Incrementing to include `kid` entry. - let map_len = u64::try_from(metadata_fields.len().saturating_add(1)).unwrap_or(u64::MAX); - encoder.map(map_len); - // Peeking through the metadata fields to insert `kid` in order. - loop { - let next_field = fields_iter.next(); - if next_field.is_some_and(|(encoded_key, _)| encoded_key == kid_label_bytes()) { - return Err(EncodeError::message( - "kid label found in the metadata fields", - )); - } - // Writing `kid` where it would have been with *bytewise** lexicographic order. - if next_field.is_none_or(|(encoded_key, _)| encoded_key > kid_label_bytes()) { - KID_LABEL.encode(&mut encoder, &mut ())?; - kid.encode(&mut encoder, &mut ())?; - } - let Some((encoded_key, encoded_v)) = next_field else { - break; - }; - // Writing a pre-encoded field of the map. - encoder.writer_mut().extend_from_slice(encoded_key); - encoder.writer_mut().extend_from_slice(encoded_v); - } - - Ok(ByteVec::from(encoder.into_writer())) -} diff --git a/rust/signed_doc/src/lib.rs b/rust/signed_doc/src/lib.rs index f0add61009..39908e7f11 100644 --- a/rust/signed_doc/src/lib.rs +++ b/rust/signed_doc/src/lib.rs @@ -190,11 +190,11 @@ impl InnerCatalystSignedDocument { let protected_header = Header::try_from(&self.metadata).context("Failed to encode Document Metadata")?; - let content = sself + let content = self .content .encoded_bytes(self.metadata.content_encoding())?; - let mut builder = coet::CoseSignBuilder::new() + let mut builder = coset::CoseSignBuilder::new() .protected(protected_header) .payload(content); From 322fcf66bde45e5b11e2d00d7e2fd53a56e930c5 Mon Sep 17 00:00:00 2001 From: no30bit Date: Wed, 28 May 2025 05:42:40 +0300 Subject: [PATCH 06/16] encode the document --- rust/signed_doc/src/builder.rs | 40 ++++---- rust/signed_doc/src/builder/cose.rs | 104 +++++++++++++++++++++ rust/signed_doc/src/builder/cose_header.rs | 37 -------- rust/signed_doc/src/builder/cose_sign.rs | 61 ------------ 4 files changed, 124 insertions(+), 118 deletions(-) create mode 100644 rust/signed_doc/src/builder/cose.rs delete mode 100644 rust/signed_doc/src/builder/cose_header.rs delete mode 100644 rust/signed_doc/src/builder/cose_sign.rs diff --git a/rust/signed_doc/src/builder.rs b/rust/signed_doc/src/builder.rs index 9b5f85c3f8..69eb91137b 100644 --- a/rust/signed_doc/src/builder.rs +++ b/rust/signed_doc/src/builder.rs @@ -2,32 +2,29 @@ /// An implementation of [`CborMap`]. mod cbor_map; -/// COSE protected header as per [RFC 8152](https://datatracker.ietf.org/doc/html/rfc8152#autoid-8), -/// but with some fields omitted when unused by Catalyst and some fields specialized for -/// it. -mod cose_header; -/// Signed payload with signatures as described in -/// [section 2 of RFC 8152](https://datatracker.ietf.org/doc/html/rfc8152#section-2). -/// Specialized for Catalyst Signed Document (e.g. no support for unprotected headers). -mod cose_sign; +/// COSE format utils. +mod cose; use std::convert::Infallible; use catalyst_types::catalyst_id::CatalystId; use cbor_map::CborMap; -use cose_header::{make_metadata_header, make_signature_header}; -use cose_sign::make_tbs_data; - -use crate::CatalystSignedDocument; +use cose::{ + encode_cose_sign, make_cose_signature, make_metadata_header, make_signature_header, + make_tbs_data, +}; pub type EncodeError = minicbor::encode::Error; /// Catalyst Signed Document Builder. #[derive(Debug)] pub struct Builder { + /// Mapping from encoded keys to encoded values. metadata: CborMap, + /// Encoded document content. content: Vec, - signatures: Vec<(CatalystId, Vec)>, + /// Encoded COSE Signatures. + signatures: Vec>, } impl Builder { @@ -66,25 +63,28 @@ impl Builder { pub fn add_signature( mut self, sign_fn: impl FnOnce(Vec) -> Vec, kid: CatalystId, ) -> Result { + // Question: maybe this should be cached (e.g. frozen once filled)? let metadata_header = make_metadata_header(&self.metadata); let kid_str = kid.to_string().into_bytes(); let signature_header = make_signature_header(kid_str.as_slice())?; - // Question: maybe this should be cached? let tbs_data = make_tbs_data(&metadata_header, &signature_header, &self.content)?; + let signature_bytes = sign_fn(tbs_data); - let signature = sign_fn(tbs_data); - - self.signatures.push((kid, signature)); + let signature = make_cose_signature(&signature_header, &signature_bytes)?; + self.signatures.push(signature); Ok(self) } - /// Build a signed document with the collected error report. + /// Build a CBOR-encoded signed document with the collected error report. /// Could provide an invalid document. #[must_use] - pub fn build(self) -> CatalystSignedDocument { - self.0.into() + pub fn build( + &self, e: &mut minicbor::Encoder, + ) -> Result<(), minicbor::encode::Error> { + let metadata_header = make_metadata_header(&self.metadata); + encode_cose_sign(e, &metadata_header, &self.content, &self.signatures) } } diff --git a/rust/signed_doc/src/builder/cose.rs b/rust/signed_doc/src/builder/cose.rs new file mode 100644 index 0000000000..30f21d0282 --- /dev/null +++ b/rust/signed_doc/src/builder/cose.rs @@ -0,0 +1,104 @@ +use minicbor::{ + bytes::{ByteArray, ByteSlice}, + data::Tagged, + Encode as _, +}; + +use super::{cbor_map::CborMap, EncodeError}; + +/// Make the header using the provided cbor-encoded key-value pairs representing +/// fields, conforming to the header fields specification +/// as per [RFC 8152](https://datatracker.ietf.org/doc/html/rfc8152#autoid-8). +pub fn make_metadata_header(metadata_fields: &CborMap) -> Vec { + let mut encoder = minicbor::Encoder::new(vec![]); + + let map_len = u64::try_from(metadata_fields.len()).unwrap_or(u64::MAX); + encoder.map(map_len); + for (encoded_key, encoded_v) in metadata_fields.iter() { + // Writing a pre-encoded field of the map. + encoder.writer_mut().extend_from_slice(encoded_key); + encoder.writer_mut().extend_from_slice(encoded_v); + } + + encoder.into_writer() +} + +/// Make the protected header for the `Cose_signature`, conforming to the header fields specification. +/// +/// # Errors +/// +/// - If encoding of the `kid` fails. +pub fn make_signature_header(kid: &[u8]) -> Result, EncodeError> { + /// The KID label as per [RFC 8152 3.1 section](https://datatracker.ietf.org/doc/html/rfc8152#section-3.1). + pub const KID_LABEL: u8 = 4; + + let mut encoder = minicbor::Encoder::new(vec![]); + // A map with a single `kid` field. + encoder.map(1u64)?.u8(KID_LABEL)?.bytes(kid)?; + Ok(encoder.into_writer()) +} + +/// Create a binary blob that will be signed and construct the to-be-signed data from it +/// in-place. Specialized for Catalyst Signed Document (e.g. no support for unprotected headers). +/// +/// Described in [section 2 of RFC 8152](https://datatracker.ietf.org/doc/html/rfc8152#section-2). +pub fn make_tbs_data( + metadata_header: &[u8], signature_header: &[u8], content: &[u8], +) -> Result, EncodeError> { + /// The context string as per [RFC 8152 section 4.4](https://datatracker.ietf.org/doc/html/rfc8152#section-4.4). + const SIGNATURE_CONTEXT: &str = "Signature"; + + minicbor::to_vec(( + SIGNATURE_CONTEXT, + <&ByteSlice>::from(metadata_header), + <&ByteSlice>::from(signature_header), + ByteArray::from([]), // aad. + <&ByteSlice>::from(content), + )) +} + +/// Make `Cose_signature`. +/// +/// Signature bytes should represent a cryptographic signature. +pub fn make_cose_signature( + protected_header: &[u8], signature_bytes: &[u8], +) -> Result, EncodeError> { + minicbor::to_vec([ + <&ByteSlice>::from(protected_header), + <&ByteSlice>::from(signature_bytes), + ]) +} + +/// Collect an array from an iterator of pre-encoded `Cose_signature` items. +fn collect_cose_signature_array(signatures: S) -> Result, EncodeError> +where + S: IntoIterator, IntoIter: ExactSizeIterator>, +{ + let iter = signatures.into_iter(); + let array_len = u64::try_from(iter.len().saturating_add(1)).unwrap_or(u64::MAX); + let mut encoder = minicbor::Encoder::new(vec![]); + encoder.array(array_len)?; + for signature in iter { + encoder.bytes(signature.as_ref())?; + } + Ok(encoder.into_writer()) +} + +/// Make cbor-encoded tagged `Cose_Sign`. +pub fn encode_cose_sign( + e: &mut minicbor::encode::Encoder, metadata_header: &[u8], content: &[u8], signatures: S, +) -> Result<(), minicbor::encode::Error> +where + S: IntoIterator, IntoIter: ExactSizeIterator>, +{ + /// From the table in [section 2 of RFC 8152](https://datatracker.ietf.org/doc/html/rfc8152#section-2). + const COSE_SIGN_TAG: u64 = 98; + + let tagged_array = Tagged::::new(( + <&ByteSlice>::from(metadata_header), + ByteArray::from([]), // unprotected. + <&ByteSlice>::from(content), + collect_cose_signature_array(signatures).map_err(minicbor::encode::Error::custom)?, + )); + tagged_array.encode(e, &mut ()) +} diff --git a/rust/signed_doc/src/builder/cose_header.rs b/rust/signed_doc/src/builder/cose_header.rs deleted file mode 100644 index a4294d3651..0000000000 --- a/rust/signed_doc/src/builder/cose_header.rs +++ /dev/null @@ -1,37 +0,0 @@ -use super::{cbor_map::CborMap, EncodeError}; - -/// The KID label as per [RFC 8152 3.1 section](https://datatracker.ietf.org/doc/html/rfc8152#section-3.1). -pub const KID_LABEL: u8 = 4; - -/// Since [`KID_LABEL`] fits into `0 ..= 0x17`, it is encoded as is. -const fn kid_label_bytes() -> &'static [u8] { - &[KID_LABEL] -} - -/// Make the header using the provided cbor-encoded key-value pairs representing -/// fields, conforming to the header fields specification. -pub fn make_metadata_header(metadata_fields: &CborMap) -> Vec { - let mut encoder = minicbor::Encoder::new(vec![]); - - let map_len = u64::try_from(metadata_fields.len()).unwrap_or(u64::MAX); - encoder.map(map_len); - for (encoded_key, encoded_v) in metadata_fields.iter() { - // Writing a pre-encoded field of the map. - encoder.writer_mut().extend_from_slice(encoded_key); - encoder.writer_mut().extend_from_slice(encoded_v); - } - - encoder.into_writer() -} - -/// Make the protected header for the `Cose_signature`, conforming to the header fields specification. -/// -/// # Errors -/// -/// - If encoding of the `kid` fails. -pub fn make_signature_header(kid: &[u8]) -> Result, EncodeError> { - let mut encoder = minicbor::Encoder::new(vec![]); - // A map with a single `kid` field. - encoder.map(1u64)?.u8(KID_LABEL)?.bytes(kid)?; - Ok(encoder.into_writer()) -} diff --git a/rust/signed_doc/src/builder/cose_sign.rs b/rust/signed_doc/src/builder/cose_sign.rs deleted file mode 100644 index 6658e6ac68..0000000000 --- a/rust/signed_doc/src/builder/cose_sign.rs +++ /dev/null @@ -1,61 +0,0 @@ -use minicbor::bytes::{ByteArray, ByteSlice}; - -use super::EncodeError; - -/// Make `Cose_signature`. -/// -/// Signature bytes should represent a cryptographic signature. -pub fn _make_cose_signature( - protected_header: &ByteSlice, signature_bytes: &ByteSlice, -) -> Result, EncodeError> { - minicbor::to_vec([protected_header, signature_bytes]) -} - -/// Collect an array from an iterator of pre-encoded `Cose_signature` items. -/// -/// Signature bytes should represent a cryptographic signature. -pub fn _collect_cose_signature_array(signatures: S) -> Result, EncodeError> -where - S: IntoIterator, IntoIter: ExactSizeIterator>, -{ - let iter = signatures.into_iter(); - let array_len = u64::try_from(iter.len().saturating_add(1)).unwrap_or(u64::MAX); - let mut encoder = minicbor::Encoder::new(vec![]); - encoder.array(array_len)?; - for signature in iter { - encoder.bytes(signature.as_ref())?; - } - Ok(encoder.into_writer()) -} - -/// Make cbor-encoded `Cose_Sign`. -pub fn _make_cose_sign( - protected_header: &ByteSlice, content: &ByteSlice, signatures: S, -) -> Result, EncodeError> -where - S: IntoIterator, IntoIter: ExactSizeIterator>, -{ - minicbor::to_vec(( - protected_header, - ByteArray::from([]), // unprotected. - content, - _collect_cose_signature_array(signatures)?, - )) -} - -/// Create a binary blob that will be signed and construct the to-be-signed data from it -/// in-place. -pub fn make_tbs_data( - metadata_header: &[u8], signature_header: &[u8], content: &[u8], -) -> Result, EncodeError> { - /// The context string as per [RFC 8152 section 4.4](https://datatracker.ietf.org/doc/html/rfc8152#section-4.4). - const SIGNATURE_CONTEXT: &str = "Signature"; - - minicbor::to_vec(( - SIGNATURE_CONTEXT, - <&ByteSlice>::from(metadata_header), - <&ByteSlice>::from(signature_header), - ByteArray::from([]), // aad. - <&ByteSlice>::from(content), - )) -} From a4b821b356d19175923dcca8dbd83a49e65bc27a Mon Sep 17 00:00:00 2001 From: no30bit Date: Wed, 28 May 2025 05:43:49 +0300 Subject: [PATCH 07/16] cspell --- .config/dictionaries/project.dic | 1 + 1 file changed, 1 insertion(+) diff --git a/.config/dictionaries/project.dic b/.config/dictionaries/project.dic index 79bec3d502..ce6a8ad4f9 100644 --- a/.config/dictionaries/project.dic +++ b/.config/dictionaries/project.dic @@ -326,3 +326,4 @@ xprivate XPRV xpub yoroi +bytewise From b78ed407681cd1bd71a19836b0f0bfd919d5cc29 Mon Sep 17 00:00:00 2001 From: no30bit Date: Thu, 29 May 2025 13:30:10 +0300 Subject: [PATCH 08/16] split CoseSign building --- rust/signed_doc/Cargo.toml | 1 + rust/signed_doc/bins/mk_signed_doc.rs | 4 +- rust/signed_doc/src/builder.rs | 153 +++++++++++++----- rust/signed_doc/src/builder/cose.rs | 27 ++-- rust/signed_doc/src/lib.rs | 4 +- rust/signed_doc/src/validator/mod.rs | 10 +- .../src/validator/rules/content_encoding.rs | 6 +- .../src/validator/rules/content_type.rs | 12 +- .../signed_doc/src/validator/rules/doc_ref.rs | 24 +-- .../src/validator/rules/parameters.rs | 24 +-- rust/signed_doc/src/validator/rules/reply.rs | 32 ++-- .../signed_doc/src/validator/rules/section.rs | 12 +- .../src/validator/rules/signature_kid.rs | 4 +- .../src/validator/rules/template.rs | 46 +++--- rust/signed_doc/tests/comment.rs | 2 +- rust/signed_doc/tests/common/mod.rs | 4 +- rust/signed_doc/tests/decoding.rs | 6 +- rust/signed_doc/tests/signature.rs | 2 +- 18 files changed, 224 insertions(+), 149 deletions(-) diff --git a/rust/signed_doc/Cargo.toml b/rust/signed_doc/Cargo.toml index 256a37362f..8d12e5e72f 100644 --- a/rust/signed_doc/Cargo.toml +++ b/rust/signed_doc/Cargo.toml @@ -30,6 +30,7 @@ futures = "0.3.31" ed25519-bip32 = "0.4.1" # used by the `mk_signed_doc` cli tool tracing = "0.1.40" thiserror = "2.0.11" +indexmap = "2.9.0" [dev-dependencies] base64-url = "3.0.0" diff --git a/rust/signed_doc/bins/mk_signed_doc.rs b/rust/signed_doc/bins/mk_signed_doc.rs index a22b1ddf91..7764e8ba5e 100644 --- a/rust/signed_doc/bins/mk_signed_doc.rs +++ b/rust/signed_doc/bins/mk_signed_doc.rs @@ -9,7 +9,7 @@ use std::{ }; use anyhow::Context; -use catalyst_signed_doc::{Builder, CatalystId, CatalystSignedDocument}; +use catalyst_signed_doc::{CoseSignBuilder, CatalystId, CatalystSignedDocument}; use clap::Parser; fn main() { @@ -66,7 +66,7 @@ impl Cli { // Possibly encode if Metadata has an encoding set. let payload = serde_json::to_vec(&json_doc)?; // Start with no signatures. - let signed_doc = Builder::new() + let signed_doc = CoseSignBuilder::new() .with_decoded_content(payload) .with_json_metadata(metadata)? .build(); diff --git a/rust/signed_doc/src/builder.rs b/rust/signed_doc/src/builder.rs index 69eb91137b..445d6600ce 100644 --- a/rust/signed_doc/src/builder.rs +++ b/rust/signed_doc/src/builder.rs @@ -5,86 +5,155 @@ mod cbor_map; /// COSE format utils. mod cose; -use std::convert::Infallible; +use std::{convert::Infallible, fmt::Debug, sync::Arc}; use catalyst_types::catalyst_id::CatalystId; -use cbor_map::CborMap; + use cose::{ - encode_cose_sign, make_cose_signature, make_metadata_header, make_signature_header, - make_tbs_data, + encode_cose_sign, make_cose_signature, make_header, make_signature_header, make_tbs_data, }; +use indexmap::IndexMap; pub type EncodeError = minicbor::encode::Error; -/// Catalyst Signed Document Builder. -#[derive(Debug)] -pub struct Builder { - /// Mapping from encoded keys to encoded values. - metadata: CborMap, - /// Encoded document content. - content: Vec, - /// Encoded COSE Signatures. - signatures: Vec>, +/// [RFC9052-CoseSign] builder. +/// +/// [RFC9052-CoseSign]: https://datatracker.ietf.org/doc/html/rfc9052#name-signing-with-one-or-more-si +#[derive(Debug, Default)] +pub struct CoseSignBuilder { + /// Mapping from encoded keys to encoded values within COSE protected header. + protected: IndexMap, Vec>, + /// Encoded COSE payload. + payload: Option>, } -impl Builder { +impl CoseSignBuilder { /// Start building a signed document. - /// - /// Sets document content bytes. If content is encoded, it should be aligned with the + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// Sets COSE payload bytes. If content is encoded, it should be aligned with the /// encoding algorithm from the `content-encoding` field. #[must_use] - pub fn from_content(content: Vec) -> Self { - Self { - metadata: CborMap::default(), - content: content.into(), - signatures: vec![], - } + pub fn with_payload(&mut self, payload: T) -> &mut Self + where + Arc<[u8]>: From, + { + self.payload = Some(payload.into()); + self } - /// Set document field metadata. + /// Add a field to the protected header. + /// + /// If the key is already present, the value is updated. /// /// # Errors + /// /// - Fails if it the CBOR encoding fails. - pub fn add_metadata_field, V: minicbor::Encode>( - mut self, ctx: &mut C, key: K, v: V, - ) -> Result { - // Ignoring pre-insert existence of the key. - let _: Option<_> = self.metadata.encode_and_insert(ctx, key, v)?; + /// - Fails if the key is already present. + pub fn add_protected_field( + &mut self, ctx: &mut C, key: K, v: V, + ) -> Result<&mut Self, EncodeError> + where + K: minicbor::Encode + Debug, + V: minicbor::Encode, + { + let (encoded_key, encoded_v) = ( + minicbor::to_vec_with(&key, ctx)?, + minicbor::to_vec_with(v, ctx)?, + ); + let indexmap::map::Entry::Vacant(entry) = self.protected.entry(encoded_key) else { + return Err(EncodeError::message(format!( + "Trying to build a CoseSign with duplicate metadata keys (key: {key:?})" + ))); + }; + entry.insert(encoded_v); Ok(self) } - /// Add a signature to the document + /// Encode [`Self::metadata`] by [`make_metadata_header`] with fields in insertion order. + // Question: maybe this should be cached (e.g. frozen once filled)? + fn encode_protected_header(&self) -> Vec { + // This iterates in insertion order. + let metadata_fields = self + .protected + .iter() + .map(|(key, v)| (key.as_slice(), v.as_slice())); + make_header(metadata_fields) + } + + fn signer(&self) -> CoseSign { + let protected = self.encode_protected_header(); + CoseSign { + protected, + payload: self.payload.clone(), + signatures: vec![], + } + } + + /// Add a signature. + /// + /// Returns [`CoseSign`], which implements [`minicbor::Encode`]. + /// More signatures can then be added with [`CoseSign::add_signature`]. /// /// # Errors /// /// Fails if a `CatalystSignedDocument` cannot be created due to missing metadata or /// content, due to malformed data, or when the signed document cannot be /// converted into `coset::CoseSign`. - pub fn add_signature( - mut self, sign_fn: impl FnOnce(Vec) -> Vec, kid: CatalystId, - ) -> Result { - // Question: maybe this should be cached (e.g. frozen once filled)? - let metadata_header = make_metadata_header(&self.metadata); + pub fn add_signature) -> Vec>( + &self, kid: CatalystId, sign_fn: F, + ) -> Result { + let mut signer = self.signer(); + signer.add_signature(kid, sign_fn)?; + Ok(signer) + } +} +/// [RFC9052-CoseSign](https://datatracker.ietf.org/doc/html/rfc9052). +pub struct CoseSign { + /// Encoded COSE protected header. + protected: Vec, + /// Encoded COSE payload. + payload: Option>, + /// Encoded COSE signatures. + signatures: Vec>, +} + +impl CoseSign { + /// Add another signature to the [`CoseSign`]. + /// + /// # Errors + /// + /// - If CBOR encoding of the [`CatalystId`] fails. + pub fn add_signature) -> Vec>( + &mut self, kid: CatalystId, sign_fn: F, + ) -> Result<&mut Self, EncodeError> { let kid_str = kid.to_string().into_bytes(); let signature_header = make_signature_header(kid_str.as_slice())?; - let tbs_data = make_tbs_data(&metadata_header, &signature_header, &self.content)?; + let tbs_data = make_tbs_data(&self.protected, &signature_header, self.payload.as_deref())?; let signature_bytes = sign_fn(tbs_data); + // This shouldn't fail. let signature = make_cose_signature(&signature_header, &signature_bytes)?; self.signatures.push(signature); Ok(self) } +} - /// Build a CBOR-encoded signed document with the collected error report. - /// Could provide an invalid document. - #[must_use] - pub fn build( - &self, e: &mut minicbor::Encoder, +impl minicbor::Encode for CoseSign { + fn encode( + &self, e: &mut minicbor::Encoder, _: &mut C, ) -> Result<(), minicbor::encode::Error> { - let metadata_header = make_metadata_header(&self.metadata); - encode_cose_sign(e, &metadata_header, &self.content, &self.signatures) + encode_cose_sign( + e, + &self.protected, + self.payload.as_deref(), + &self.signatures, + ) } } diff --git a/rust/signed_doc/src/builder/cose.rs b/rust/signed_doc/src/builder/cose.rs index 30f21d0282..b81f8df062 100644 --- a/rust/signed_doc/src/builder/cose.rs +++ b/rust/signed_doc/src/builder/cose.rs @@ -4,17 +4,22 @@ use minicbor::{ Encode as _, }; -use super::{cbor_map::CborMap, EncodeError}; +use super::EncodeError; /// Make the header using the provided cbor-encoded key-value pairs representing /// fields, conforming to the header fields specification /// as per [RFC 8152](https://datatracker.ietf.org/doc/html/rfc8152#autoid-8). -pub fn make_metadata_header(metadata_fields: &CborMap) -> Vec { +pub fn make_header<'a, I>(encoded_fields: I) -> Vec +where + I: IntoIterator, +{ let mut encoder = minicbor::Encoder::new(vec![]); - let map_len = u64::try_from(metadata_fields.len()).unwrap_or(u64::MAX); + let iter = encoded_fields.into_iter(); + let map_len = u64::try_from(iter.len()).unwrap_or(u64::MAX); encoder.map(map_len); - for (encoded_key, encoded_v) in metadata_fields.iter() { + + for (encoded_key, encoded_v) in iter { // Writing a pre-encoded field of the map. encoder.writer_mut().extend_from_slice(encoded_key); encoder.writer_mut().extend_from_slice(encoded_v); @@ -43,7 +48,7 @@ pub fn make_signature_header(kid: &[u8]) -> Result, EncodeError> { /// /// Described in [section 2 of RFC 8152](https://datatracker.ietf.org/doc/html/rfc8152#section-2). pub fn make_tbs_data( - metadata_header: &[u8], signature_header: &[u8], content: &[u8], + metadata_header: &[u8], signature_header: &[u8], content: Option<&[u8]>, ) -> Result, EncodeError> { /// The context string as per [RFC 8152 section 4.4](https://datatracker.ietf.org/doc/html/rfc8152#section-4.4). const SIGNATURE_CONTEXT: &str = "Signature"; @@ -52,8 +57,8 @@ pub fn make_tbs_data( SIGNATURE_CONTEXT, <&ByteSlice>::from(metadata_header), <&ByteSlice>::from(signature_header), - ByteArray::from([]), // aad. - <&ByteSlice>::from(content), + ByteArray::from([]), // no aad. + <&ByteSlice>::from(content.unwrap_or(&[])), // allowing no payload (i.e. no content). )) } @@ -86,7 +91,7 @@ where /// Make cbor-encoded tagged `Cose_Sign`. pub fn encode_cose_sign( - e: &mut minicbor::encode::Encoder, metadata_header: &[u8], content: &[u8], signatures: S, + e: &mut minicbor::encode::Encoder, protected: &[u8], payload: Option<&[u8]>, signatures: S, ) -> Result<(), minicbor::encode::Error> where S: IntoIterator, IntoIter: ExactSizeIterator>, @@ -95,9 +100,9 @@ where const COSE_SIGN_TAG: u64 = 98; let tagged_array = Tagged::::new(( - <&ByteSlice>::from(metadata_header), - ByteArray::from([]), // unprotected. - <&ByteSlice>::from(content), + <&ByteSlice>::from(protected), + ByteArray::from([]), // unprotected. + payload.map(<&ByteSlice>::from), // allowing `NULL`. collect_cose_signature_array(signatures).map_err(minicbor::encode::Error::custom)?, )); tagged_array.encode(e, &mut ()) diff --git a/rust/signed_doc/src/lib.rs b/rust/signed_doc/src/lib.rs index 75e0681a48..772aaa0299 100644 --- a/rust/signed_doc/src/lib.rs +++ b/rust/signed_doc/src/lib.rs @@ -16,7 +16,7 @@ use std::{ }; use anyhow::Context; -pub use builder::Builder; +pub use builder::CoseSignBuilder; pub use catalyst_types::{ problem_report::ProblemReport, uuid::{Uuid, UuidV4, UuidV7}, @@ -184,7 +184,7 @@ impl CatalystSignedDocument { /// Returns a signed document `Builder` pre-loaded with the current signed document's /// data. #[must_use] - pub fn into_builder(&self) -> Builder { + pub fn into_builder(&self) -> CoseSignBuilder { self.into() } } diff --git a/rust/signed_doc/src/validator/mod.rs b/rust/signed_doc/src/validator/mod.rs index 0c755bdcb5..e20389f981 100644 --- a/rust/signed_doc/src/validator/mod.rs +++ b/rust/signed_doc/src/validator/mod.rs @@ -362,7 +362,7 @@ mod tests { use crate::{ providers::{tests::TestCatalystSignedDocumentProvider, CatalystSignedDocumentProvider}, validator::{document_rules_init, validate_id_and_ver}, - Builder, UuidV7, + CoseSignBuilder, UuidV7, }; #[test] @@ -374,7 +374,7 @@ mod tests { .as_secs(); let uuid_v7 = UuidV7::new(); - let doc = Builder::new() + let doc = CoseSignBuilder::new() .with_json_metadata(serde_json::json!({ "id": uuid_v7.to_string(), "ver": uuid_v7.to_string() @@ -388,7 +388,7 @@ mod tests { let ver = Uuid::new_v7(Timestamp::from_unix_time(now - 1, 0, 0, 0)); let id = Uuid::new_v7(Timestamp::from_unix_time(now + 1, 0, 0, 0)); assert!(ver < id); - let doc = Builder::new() + let doc = CoseSignBuilder::new() .with_json_metadata(serde_json::json!({ "id": id.to_string(), "ver": ver.to_string() @@ -405,7 +405,7 @@ mod tests { 0, 0, )); - let doc = Builder::new() + let doc = CoseSignBuilder::new() .with_json_metadata(serde_json::json!({ "id": to_far_in_past.to_string(), "ver": to_far_in_past.to_string() @@ -422,7 +422,7 @@ mod tests { 0, 0, )); - let doc = Builder::new() + let doc = CoseSignBuilder::new() .with_json_metadata(serde_json::json!({ "id": to_far_in_future.to_string(), "ver": to_far_in_future.to_string() diff --git a/rust/signed_doc/src/validator/rules/content_encoding.rs b/rust/signed_doc/src/validator/rules/content_encoding.rs index f75bcf81e3..be6986fed6 100644 --- a/rust/signed_doc/src/validator/rules/content_encoding.rs +++ b/rust/signed_doc/src/validator/rules/content_encoding.rs @@ -38,7 +38,7 @@ impl ContentEncodingRule { #[cfg(test)] mod tests { use super::*; - use crate::Builder; + use crate::CoseSignBuilder; #[tokio::test] async fn content_encoding_rule_test() { @@ -49,7 +49,7 @@ mod tests { optional: true, }; - let doc = Builder::new() + let doc = CoseSignBuilder::new() .with_json_metadata( serde_json::json!({"content-encoding": content_encoding.to_string() }), ) @@ -57,7 +57,7 @@ mod tests { .build(); assert!(rule.check(&doc).await.unwrap()); - let doc = Builder::new() + let doc = CoseSignBuilder::new() .with_json_metadata(serde_json::json!({})) .unwrap() .build(); diff --git a/rust/signed_doc/src/validator/rules/content_type.rs b/rust/signed_doc/src/validator/rules/content_type.rs index 26aa702fa7..364b1414ed 100644 --- a/rust/signed_doc/src/validator/rules/content_type.rs +++ b/rust/signed_doc/src/validator/rules/content_type.rs @@ -53,7 +53,7 @@ impl ContentTypeRule { #[cfg(test)] mod tests { use super::*; - use crate::Builder; + use crate::CoseSignBuilder; #[tokio::test] async fn content_type_rule_test() { @@ -61,34 +61,34 @@ mod tests { let rule = ContentTypeRule { exp: content_type }; - let doc = Builder::new() + let doc = CoseSignBuilder::new() .with_json_metadata(serde_json::json!({"content-type": content_type.to_string() })) .unwrap() .with_decoded_content(serde_json::to_vec(&serde_json::json!({})).unwrap()) .build(); assert!(rule.check(&doc).await.unwrap()); - let doc = Builder::new() + let doc = CoseSignBuilder::new() .with_json_metadata(serde_json::json!({"content-type": ContentType::Cbor.to_string() })) .unwrap() .with_decoded_content(serde_json::to_vec(&serde_json::json!({})).unwrap()) .build(); assert!(!rule.check(&doc).await.unwrap()); - let doc = Builder::new() + let doc = CoseSignBuilder::new() .with_json_metadata(serde_json::json!({"content-type": content_type.to_string() })) .unwrap() .build(); assert!(!rule.check(&doc).await.unwrap()); - let doc = Builder::new() + let doc = CoseSignBuilder::new() .with_json_metadata(serde_json::json!({"content-type": content_type.to_string() })) .unwrap() .with_decoded_content(vec![]) .build(); assert!(!rule.check(&doc).await.unwrap()); - let doc = Builder::new().build(); + let doc = CoseSignBuilder::new().build(); assert!(!rule.check(&doc).await.unwrap()); } } diff --git a/rust/signed_doc/src/validator/rules/doc_ref.rs b/rust/signed_doc/src/validator/rules/doc_ref.rs index 53fec6825f..1cb95c4cab 100644 --- a/rust/signed_doc/src/validator/rules/doc_ref.rs +++ b/rust/signed_doc/src/validator/rules/doc_ref.rs @@ -86,7 +86,7 @@ mod tests { use catalyst_types::uuid::UuidV7; use super::*; - use crate::{providers::tests::TestCatalystSignedDocumentProvider, Builder}; + use crate::{providers::tests::TestCatalystSignedDocumentProvider, CoseSignBuilder}; #[tokio::test] async fn ref_rule_specified_test() { @@ -103,7 +103,7 @@ mod tests { // prepare replied documents { - let ref_doc = Builder::new() + let ref_doc = CoseSignBuilder::new() .with_json_metadata(serde_json::json!({ "id": valid_referenced_doc_id.to_string(), "ver": valid_referenced_doc_ver.to_string(), @@ -114,7 +114,7 @@ mod tests { provider.add_document(ref_doc).unwrap(); // reply doc with other `type` field - let ref_doc = Builder::new() + let ref_doc = CoseSignBuilder::new() .with_json_metadata(serde_json::json!({ "id": another_type_referenced_doc_id.to_string(), "ver": another_type_referenced_doc_ver.to_string(), @@ -125,7 +125,7 @@ mod tests { provider.add_document(ref_doc).unwrap(); // missing `type` field in the referenced document - let ref_doc = Builder::new() + let ref_doc = CoseSignBuilder::new() .with_json_metadata(serde_json::json!({ "id": missing_type_referenced_doc_id.to_string(), "ver": missing_type_referenced_doc_ver.to_string(), @@ -140,7 +140,7 @@ mod tests { exp_ref_type, optional: false, }; - let doc = Builder::new() + let doc = CoseSignBuilder::new() .with_json_metadata(serde_json::json!({ "ref": {"id": valid_referenced_doc_id.to_string(), "ver": valid_referenced_doc_ver.to_string() } })) @@ -153,7 +153,7 @@ mod tests { exp_ref_type, optional: true, }; - let doc = Builder::new().build(); + let doc = CoseSignBuilder::new().build(); assert!(rule.check(&doc, &provider).await.unwrap()); // missing `ref` field, but its required @@ -161,11 +161,11 @@ mod tests { exp_ref_type, optional: false, }; - let doc = Builder::new().build(); + let doc = CoseSignBuilder::new().build(); assert!(!rule.check(&doc, &provider).await.unwrap()); // reference to the document with another `type` field - let doc = Builder::new() + let doc = CoseSignBuilder::new() .with_json_metadata(serde_json::json!({ "ref": {"id": another_type_referenced_doc_id.to_string(), "ver": another_type_referenced_doc_ver.to_string() } })) @@ -174,7 +174,7 @@ mod tests { assert!(!rule.check(&doc, &provider).await.unwrap()); // missing `type` field in the referenced document - let doc = Builder::new() + let doc = CoseSignBuilder::new() .with_json_metadata(serde_json::json!({ "ref": {"id": missing_type_referenced_doc_id.to_string(), "ver": missing_type_referenced_doc_ver.to_string() } })) @@ -183,7 +183,7 @@ mod tests { assert!(!rule.check(&doc, &provider).await.unwrap()); // cannot find a referenced document - let doc = Builder::new() + let doc = CoseSignBuilder::new() .with_json_metadata(serde_json::json!({ "ref": {"id": UuidV7::new().to_string(), "ver": UuidV7::new().to_string() } })) @@ -197,12 +197,12 @@ mod tests { let rule = RefRule::NotSpecified; let provider = TestCatalystSignedDocumentProvider::default(); - let doc = Builder::new().build(); + let doc = CoseSignBuilder::new().build(); assert!(rule.check(&doc, &provider).await.unwrap()); let ref_id = UuidV7::new(); let ref_ver = UuidV7::new(); - let doc = Builder::new() + let doc = CoseSignBuilder::new() .with_json_metadata(serde_json::json!({"ref": {"id": ref_id.to_string(), "ver": ref_ver.to_string() } })) .unwrap() .build(); diff --git a/rust/signed_doc/src/validator/rules/parameters.rs b/rust/signed_doc/src/validator/rules/parameters.rs index 290d158439..a600d2cbf0 100644 --- a/rust/signed_doc/src/validator/rules/parameters.rs +++ b/rust/signed_doc/src/validator/rules/parameters.rs @@ -75,7 +75,7 @@ mod tests { use catalyst_types::uuid::{UuidV4, UuidV7}; use super::*; - use crate::{providers::tests::TestCatalystSignedDocumentProvider, Builder}; + use crate::{providers::tests::TestCatalystSignedDocumentProvider, CoseSignBuilder}; #[tokio::test] async fn ref_rule_specified_test() { @@ -92,7 +92,7 @@ mod tests { // prepare replied documents { - let ref_doc = Builder::new() + let ref_doc = CoseSignBuilder::new() .with_json_metadata(serde_json::json!({ "id": valid_category_doc_id.to_string(), "ver": valid_category_doc_ver.to_string(), @@ -103,7 +103,7 @@ mod tests { provider.add_document(ref_doc).unwrap(); // reply doc with other `type` field - let ref_doc = Builder::new() + let ref_doc = CoseSignBuilder::new() .with_json_metadata(serde_json::json!({ "id": another_type_category_doc_id.to_string(), "ver": another_type_category_doc_ver.to_string(), @@ -114,7 +114,7 @@ mod tests { provider.add_document(ref_doc).unwrap(); // missing `type` field in the referenced document - let ref_doc = Builder::new() + let ref_doc = CoseSignBuilder::new() .with_json_metadata(serde_json::json!({ "id": missing_type_category_doc_id.to_string(), "ver": missing_type_category_doc_ver.to_string(), @@ -129,7 +129,7 @@ mod tests { exp_parameters_type, optional: false, }; - let doc = Builder::new() + let doc = CoseSignBuilder::new() .with_json_metadata(serde_json::json!({ "parameters": {"id": valid_category_doc_id.to_string(), "ver": valid_category_doc_ver } })) @@ -142,7 +142,7 @@ mod tests { exp_parameters_type, optional: true, }; - let doc = Builder::new().build(); + let doc = CoseSignBuilder::new().build(); assert!(rule.check(&doc, &provider).await.unwrap()); // missing `parameters` field, but its required @@ -150,11 +150,11 @@ mod tests { exp_parameters_type, optional: false, }; - let doc = Builder::new().build(); + let doc = CoseSignBuilder::new().build(); assert!(!rule.check(&doc, &provider).await.unwrap()); // reference to the document with another `type` field - let doc = Builder::new() + let doc = CoseSignBuilder::new() .with_json_metadata(serde_json::json!({ "parameters": {"id": another_type_category_doc_id.to_string(), "ver": another_type_category_doc_ver.to_string() } })) @@ -163,7 +163,7 @@ mod tests { assert!(!rule.check(&doc, &provider).await.unwrap()); // missing `type` field in the referenced document - let doc = Builder::new() + let doc = CoseSignBuilder::new() .with_json_metadata(serde_json::json!({ "parameters": {"id": missing_type_category_doc_id.to_string(), "ver": missing_type_category_doc_ver.to_string() } })) @@ -172,7 +172,7 @@ mod tests { assert!(!rule.check(&doc, &provider).await.unwrap()); // cannot find a referenced document - let doc = Builder::new() + let doc = CoseSignBuilder::new() .with_json_metadata(serde_json::json!({ "parameters": {"id": UuidV7::new().to_string(), "ver": UuidV7::new().to_string() } })) @@ -186,12 +186,12 @@ mod tests { let rule = ParametersRule::NotSpecified; let provider = TestCatalystSignedDocumentProvider::default(); - let doc = Builder::new().build(); + let doc = CoseSignBuilder::new().build(); assert!(rule.check(&doc, &provider).await.unwrap()); let ref_id = UuidV7::new(); let ref_ver = UuidV7::new(); - let doc = Builder::new() + let doc = CoseSignBuilder::new() .with_json_metadata(serde_json::json!({"parameters": {"id": ref_id.to_string(), "ver": ref_ver.to_string() } })) .unwrap() .build(); diff --git a/rust/signed_doc/src/validator/rules/reply.rs b/rust/signed_doc/src/validator/rules/reply.rs index 5ac256667d..1c6c5817cb 100644 --- a/rust/signed_doc/src/validator/rules/reply.rs +++ b/rust/signed_doc/src/validator/rules/reply.rs @@ -95,7 +95,7 @@ mod tests { use catalyst_types::uuid::{UuidV4, UuidV7}; use super::*; - use crate::{providers::tests::TestCatalystSignedDocumentProvider, Builder}; + use crate::{providers::tests::TestCatalystSignedDocumentProvider, CoseSignBuilder}; #[allow(clippy::too_many_lines)] #[tokio::test] @@ -117,7 +117,7 @@ mod tests { // prepare replied documents { - let ref_doc = Builder::new() + let ref_doc = CoseSignBuilder::new() .with_json_metadata(serde_json::json!({ "ref": { "id": common_ref_id.to_string(), "ver": common_ref_ver.to_string() }, "id": valid_replied_doc_id.to_string(), @@ -129,7 +129,7 @@ mod tests { provider.add_document(ref_doc).unwrap(); // reply doc with other `type` field - let ref_doc = Builder::new() + let ref_doc = CoseSignBuilder::new() .with_json_metadata(serde_json::json!({ "ref": { "id": common_ref_id.to_string(), "ver": common_ref_ver.to_string() }, "id": another_type_replied_doc_id.to_string(), @@ -141,7 +141,7 @@ mod tests { provider.add_document(ref_doc).unwrap(); // missing `ref` field in the referenced document - let ref_doc = Builder::new() + let ref_doc = CoseSignBuilder::new() .with_json_metadata(serde_json::json!({ "id": missing_ref_replied_doc_id.to_string(), "ver": missing_ref_replied_doc_ver.to_string(), @@ -152,7 +152,7 @@ mod tests { provider.add_document(ref_doc).unwrap(); // missing `type` field in the referenced document - let ref_doc = Builder::new() + let ref_doc = CoseSignBuilder::new() .with_json_metadata(serde_json::json!({ "ref": { "id": common_ref_id.to_string(), "ver": common_ref_ver.to_string() }, "id": missing_type_replied_doc_id.to_string(), @@ -168,7 +168,7 @@ mod tests { exp_reply_type, optional: false, }; - let doc = Builder::new() + let doc = CoseSignBuilder::new() .with_json_metadata(serde_json::json!({ "ref": { "id": common_ref_id.to_string(), "ver": common_ref_ver.to_string() }, "reply": { "id": valid_replied_doc_id.to_string(), "ver": valid_replied_doc_ver.to_string() } @@ -182,7 +182,7 @@ mod tests { exp_reply_type, optional: true, }; - let doc = Builder::new().build(); + let doc = CoseSignBuilder::new().build(); assert!(rule.check(&doc, &provider).await.unwrap()); // missing `reply` field, but its required @@ -190,7 +190,7 @@ mod tests { exp_reply_type, optional: false, }; - let doc = Builder::new() + let doc = CoseSignBuilder::new() .with_json_metadata(serde_json::json!({ "ref": { "id": common_ref_id.to_string(), "ver": common_ref_ver.to_string() }, })) @@ -199,7 +199,7 @@ mod tests { assert!(!rule.check(&doc, &provider).await.unwrap()); // missing `ref` field - let doc = Builder::new() + let doc = CoseSignBuilder::new() .with_json_metadata(serde_json::json!({ "reply": { "id": valid_replied_doc_id.to_string(), "ver": valid_replied_doc_ver.to_string() } })) @@ -208,7 +208,7 @@ mod tests { assert!(!rule.check(&doc, &provider).await.unwrap()); // reference to the document with another `type` field - let doc = Builder::new() + let doc = CoseSignBuilder::new() .with_json_metadata(serde_json::json!({ "ref": { "id": common_ref_id.to_string(), "ver": common_ref_ver.to_string() }, "reply": { "id": another_type_replied_doc_id.to_string(), "ver": another_type_replied_doc_ver.to_string() } @@ -218,7 +218,7 @@ mod tests { assert!(!rule.check(&doc, &provider).await.unwrap()); // missing `ref` field in the referenced document - let doc = Builder::new() + let doc = CoseSignBuilder::new() .with_json_metadata(serde_json::json!({ "ref": { "id": common_ref_id.to_string(), "ver": common_ref_ver.to_string() }, "reply": { "id": missing_ref_replied_doc_id.to_string(), "ver": missing_type_replied_doc_ver.to_string() } @@ -228,7 +228,7 @@ mod tests { assert!(!rule.check(&doc, &provider).await.unwrap()); // missing `type` field in the referenced document - let doc = Builder::new() + let doc = CoseSignBuilder::new() .with_json_metadata(serde_json::json!({ "ref": { "id": common_ref_id.to_string(), "ver": common_ref_ver.to_string() }, "reply": { "id": missing_type_replied_doc_id.to_string(), "ver": missing_type_replied_doc_ver.to_string() } @@ -238,7 +238,7 @@ mod tests { assert!(!rule.check(&doc, &provider).await.unwrap()); // `ref` field does not align with the referenced document - let doc = Builder::new() + let doc = CoseSignBuilder::new() .with_json_metadata(serde_json::json!({ "ref": { "id": UuidV7::new().to_string(), "ver": UuidV7::new().to_string() }, "reply": { "id": valid_replied_doc_id.to_string(), "ver": valid_replied_doc_ver.to_string() } @@ -248,7 +248,7 @@ mod tests { assert!(!rule.check(&doc, &provider).await.unwrap()); // cannot find a referenced document - let doc = Builder::new() + let doc = CoseSignBuilder::new() .with_json_metadata(serde_json::json!({ "ref": { "id": common_ref_id.to_string(), "ver": common_ref_ver.to_string() }, "reply": {"id": UuidV7::new().to_string(), "ver": UuidV7::new().to_string() } @@ -263,12 +263,12 @@ mod tests { let rule = ReplyRule::NotSpecified; let provider = TestCatalystSignedDocumentProvider::default(); - let doc = Builder::new().build(); + let doc = CoseSignBuilder::new().build(); assert!(rule.check(&doc, &provider).await.unwrap()); let ref_id = UuidV7::new(); let ref_ver = UuidV7::new(); - let doc = Builder::new() + let doc = CoseSignBuilder::new() .with_json_metadata(serde_json::json!({"reply": {"id": ref_id.to_string(), "ver": ref_ver.to_string() } })) .unwrap() .build(); diff --git a/rust/signed_doc/src/validator/rules/section.rs b/rust/signed_doc/src/validator/rules/section.rs index 8720353425..05c2ea5e25 100644 --- a/rust/signed_doc/src/validator/rules/section.rs +++ b/rust/signed_doc/src/validator/rules/section.rs @@ -42,11 +42,11 @@ impl SectionRule { #[cfg(test)] mod tests { use super::*; - use crate::Builder; + use crate::CoseSignBuilder; #[tokio::test] async fn section_rule_specified_test() { - let doc = Builder::new() + let doc = CoseSignBuilder::new() .with_json_metadata(serde_json::json!({ "section": "$".to_string() })) @@ -55,11 +55,11 @@ mod tests { let rule = SectionRule::Specified { optional: false }; assert!(rule.check(&doc).await.unwrap()); - let doc = Builder::new().build(); + let doc = CoseSignBuilder::new().build(); let rule = SectionRule::Specified { optional: true }; assert!(rule.check(&doc).await.unwrap()); - let doc = Builder::new().build(); + let doc = CoseSignBuilder::new().build(); let rule = SectionRule::Specified { optional: false }; assert!(!rule.check(&doc).await.unwrap()); } @@ -68,10 +68,10 @@ mod tests { async fn section_rule_not_specified_test() { let rule = SectionRule::NotSpecified; - let doc = Builder::new().build(); + let doc = CoseSignBuilder::new().build(); assert!(rule.check(&doc).await.unwrap()); - let doc = Builder::new() + let doc = CoseSignBuilder::new() .with_json_metadata(serde_json::json!({ "section": "$".to_string() })) diff --git a/rust/signed_doc/src/validator/rules/signature_kid.rs b/rust/signed_doc/src/validator/rules/signature_kid.rs index 2e45517b8e..afb8b4cf05 100644 --- a/rust/signed_doc/src/validator/rules/signature_kid.rs +++ b/rust/signed_doc/src/validator/rules/signature_kid.rs @@ -47,7 +47,7 @@ mod tests { use ed25519_dalek::ed25519::signature::Signer; use super::*; - use crate::{Builder, ContentType}; + use crate::{CoseSignBuilder, ContentType}; #[tokio::test] async fn signature_kid_rule_test() { @@ -59,7 +59,7 @@ mod tests { let pk = sk.verifying_key(); let kid = CatalystId::new("cardano", None, pk).with_role(RoleId::Role0); - let doc = Builder::new() + let doc = CoseSignBuilder::new() .with_decoded_content(serde_json::to_vec(&serde_json::Value::Null).unwrap()) .with_json_metadata(serde_json::json!({ "type": UuidV4::new().to_string(), diff --git a/rust/signed_doc/src/validator/rules/template.rs b/rust/signed_doc/src/validator/rules/template.rs index 0d5b0c9aaa..344dbaed6e 100644 --- a/rust/signed_doc/src/validator/rules/template.rs +++ b/rust/signed_doc/src/validator/rules/template.rs @@ -184,7 +184,7 @@ mod tests { use catalyst_types::uuid::UuidV7; use super::*; - use crate::{providers::tests::TestCatalystSignedDocumentProvider, Builder}; + use crate::{providers::tests::TestCatalystSignedDocumentProvider, CoseSignBuilder}; #[allow(clippy::too_many_lines)] #[tokio::test] @@ -205,7 +205,7 @@ mod tests { // prepare replied documents { - let ref_doc = Builder::new() + let ref_doc = CoseSignBuilder::new() .with_json_metadata(serde_json::json!({ "id": valid_template_doc_id.to_string(), "ver": valid_template_doc_id.to_string(), @@ -218,7 +218,7 @@ mod tests { provider.add_document(ref_doc).unwrap(); // reply doc with other `type` field - let ref_doc = Builder::new() + let ref_doc = CoseSignBuilder::new() .with_json_metadata(serde_json::json!({ "id": another_type_template_doc_id.to_string(), "ver": another_type_template_doc_id.to_string(), @@ -231,7 +231,7 @@ mod tests { provider.add_document(ref_doc).unwrap(); // missing `type` field in the referenced document - let ref_doc = Builder::new() + let ref_doc = CoseSignBuilder::new() .with_json_metadata(serde_json::json!({ "id": missing_type_template_doc_id.to_string(), "ver": missing_type_template_doc_id.to_string(), @@ -243,7 +243,7 @@ mod tests { provider.add_document(ref_doc).unwrap(); // missing `content-type` field in the referenced document - let ref_doc = Builder::new() + let ref_doc = CoseSignBuilder::new() .with_json_metadata(serde_json::json!({ "id": missing_content_type_template_doc_id.to_string(), "ver": missing_content_type_template_doc_id.to_string(), @@ -255,7 +255,7 @@ mod tests { provider.add_document(ref_doc).unwrap(); // missing content - let ref_doc = Builder::new() + let ref_doc = CoseSignBuilder::new() .with_json_metadata(serde_json::json!({ "id": missing_content_template_doc_id.to_string(), "ver": missing_content_template_doc_id.to_string(), @@ -267,7 +267,7 @@ mod tests { provider.add_document(ref_doc).unwrap(); // invalid content, must be json encoded - let ref_doc = Builder::new() + let ref_doc = CoseSignBuilder::new() .with_json_metadata(serde_json::json!({ "id": invalid_content_template_doc_id.to_string(), "ver": invalid_content_template_doc_id.to_string(), @@ -282,7 +282,7 @@ mod tests { // all correct let rule = ContentRule::Templated { exp_template_type }; - let doc = Builder::new() + let doc = CoseSignBuilder::new() .with_json_metadata(serde_json::json!({ "template": {"id": valid_template_doc_id.to_string(), "ver": valid_template_doc_id.to_string() } })) @@ -292,7 +292,7 @@ mod tests { assert!(rule.check(&doc, &provider).await.unwrap()); // missing `template` field, but its required - let doc = Builder::new() + let doc = CoseSignBuilder::new() .with_json_metadata(serde_json::json!({})) .unwrap() .with_decoded_content(json_content.clone()) @@ -301,7 +301,7 @@ mod tests { // missing content let rule = ContentRule::Templated { exp_template_type }; - let doc = Builder::new() + let doc = CoseSignBuilder::new() .with_json_metadata(serde_json::json!({ "template": {"id": valid_template_doc_id.to_string(), "ver": valid_template_doc_id.to_string() } })) @@ -311,7 +311,7 @@ mod tests { // content not a json encoded let rule = ContentRule::Templated { exp_template_type }; - let doc = Builder::new() + let doc = CoseSignBuilder::new() .with_json_metadata(serde_json::json!({ "template": {"id": valid_template_doc_id.to_string(), "ver": valid_template_doc_id.to_string() } })) @@ -321,7 +321,7 @@ mod tests { assert!(!rule.check(&doc, &provider).await.unwrap()); // reference to the document with another `type` field - let doc = Builder::new() + let doc = CoseSignBuilder::new() .with_json_metadata(serde_json::json!({ "template": {"id": another_type_template_doc_id.to_string(), "ver": another_type_template_doc_id.to_string() } })) @@ -331,7 +331,7 @@ mod tests { assert!(!rule.check(&doc, &provider).await.unwrap()); // missing `type` field in the referenced document - let doc = Builder::new() + let doc = CoseSignBuilder::new() .with_json_metadata(serde_json::json!({ "template": {"id": missing_type_template_doc_id.to_string(), "ver": missing_type_template_doc_id.to_string() } })) @@ -342,7 +342,7 @@ mod tests { // missing `content-type` field in the referenced doc let rule = ContentRule::Templated { exp_template_type }; - let doc = Builder::new() + let doc = CoseSignBuilder::new() .with_json_metadata(serde_json::json!({ "template": {"id": missing_content_type_template_doc_id.to_string(), "ver": missing_content_type_template_doc_id.to_string() } })) @@ -352,7 +352,7 @@ mod tests { assert!(!rule.check(&doc, &provider).await.unwrap()); // missing content in the referenced document - let doc = Builder::new() + let doc = CoseSignBuilder::new() .with_json_metadata(serde_json::json!({ "template": {"id": missing_content_template_doc_id.to_string(), "ver": missing_content_template_doc_id.to_string() } })) @@ -362,7 +362,7 @@ mod tests { assert!(!rule.check(&doc, &provider).await.unwrap()); // content not a json encoded in the referenced document - let doc = Builder::new() + let doc = CoseSignBuilder::new() .with_json_metadata(serde_json::json!({ "template": {"id": invalid_content_template_doc_id.to_string(), "ver": invalid_content_template_doc_id.to_string() } })) @@ -372,7 +372,7 @@ mod tests { assert!(!rule.check(&doc, &provider).await.unwrap()); // cannot find a referenced document - let doc = Builder::new() + let doc = CoseSignBuilder::new() .with_json_metadata(serde_json::json!({ "template": {"id": UuidV7::new().to_string(), "ver": UuidV7::new().to_string() } })) @@ -397,23 +397,23 @@ mod tests { // all correct let rule = ContentRule::Static(json_schema); - let doc = Builder::new() + let doc = CoseSignBuilder::new() .with_decoded_content(json_content.clone()) .build(); assert!(rule.check(&doc, &provider).await.unwrap()); // missing content - let doc = Builder::new().build(); + let doc = CoseSignBuilder::new().build(); assert!(!rule.check(&doc, &provider).await.unwrap()); // content not a json encoded - let doc = Builder::new().with_decoded_content(vec![]).build(); + let doc = CoseSignBuilder::new().with_decoded_content(vec![]).build(); assert!(!rule.check(&doc, &provider).await.unwrap()); // defined `template` field which should be absent let ref_id = UuidV7::new(); let ref_ver = UuidV7::new(); - let doc = Builder::new().with_decoded_content(json_content) + let doc = CoseSignBuilder::new().with_decoded_content(json_content) .with_json_metadata(serde_json::json!({"template": {"id": ref_id.to_string(), "ver": ref_ver.to_string() } })) .unwrap() .build(); @@ -425,13 +425,13 @@ mod tests { let rule = ContentRule::NotSpecified; let provider = TestCatalystSignedDocumentProvider::default(); - let doc = Builder::new().build(); + let doc = CoseSignBuilder::new().build(); assert!(rule.check(&doc, &provider).await.unwrap()); // defined `template` field which should be absent let ref_id = UuidV7::new(); let ref_ver = UuidV7::new(); - let doc = Builder::new() + let doc = CoseSignBuilder::new() .with_json_metadata(serde_json::json!({"template": {"id": ref_id.to_string(), "ver": ref_ver.to_string() } })) .unwrap() .build(); diff --git a/rust/signed_doc/tests/comment.rs b/rust/signed_doc/tests/comment.rs index 1c746e589c..30eca79e17 100644 --- a/rust/signed_doc/tests/comment.rs +++ b/rust/signed_doc/tests/comment.rs @@ -54,7 +54,7 @@ async fn test_valid_comment_doc_with_reply() { let comment_doc_id = UuidV7::new(); let comment_doc_ver = UuidV7::new(); - let comment_doc = Builder::new() + let comment_doc = CoseSignBuilder::new() .with_json_metadata(serde_json::json!({ "id": comment_doc_id, "ver": comment_doc_ver, diff --git a/rust/signed_doc/tests/common/mod.rs b/rust/signed_doc/tests/common/mod.rs index d7ea84150b..1293cdd57c 100644 --- a/rust/signed_doc/tests/common/mod.rs +++ b/rust/signed_doc/tests/common/mod.rs @@ -52,7 +52,7 @@ pub fn create_dummy_doc( let doc_id = UuidV7::new(); let doc_ver = UuidV7::new(); - let doc = Builder::new() + let doc = CoseSignBuilder::new() .with_json_metadata(serde_json::json!({ "content-type": ContentType::Json.to_string(), "type": doc_type_id, @@ -80,7 +80,7 @@ pub fn create_dummy_signed_doc( )> { let (sk, pk, kid) = create_dummy_key_pair(with_role_index)?; - let signed_doc = Builder::new() + let signed_doc = CoseSignBuilder::new() .with_decoded_content(content) .with_json_metadata(metadata)? .add_signature(|m| sk.sign(&m).to_vec(), &kid)? diff --git a/rust/signed_doc/tests/decoding.rs b/rust/signed_doc/tests/decoding.rs index 18697703c2..70167b76cf 100644 --- a/rust/signed_doc/tests/decoding.rs +++ b/rust/signed_doc/tests/decoding.rs @@ -17,7 +17,7 @@ fn catalyst_signed_doc_cbor_roundtrip_kid_as_id_test() { let content = serde_json::to_vec(&serde_json::Value::Null).unwrap(); - let doc = Builder::new() + let doc = CoseSignBuilder::new() .with_json_metadata(metadata_fields.clone()) .unwrap() .with_decoded_content(content.clone()) @@ -38,7 +38,7 @@ async fn catalyst_signed_doc_parameters_aliases_test() { let content = serde_json::to_vec(&serde_json::Value::Null).unwrap(); - let doc = Builder::new() + let doc = CoseSignBuilder::new() .with_json_metadata(metadata_fields.clone()) .unwrap() .with_decoded_content(content.clone()) @@ -188,7 +188,7 @@ fn signed_doc_with_all_fields_case() -> TestCase { bytes_gen: Box::new({ let kid = kid.clone(); move || { - Builder::new() + CoseSignBuilder::new() .with_json_metadata(serde_json::json!({ "content-type": ContentType::Json.to_string(), "content-encoding": ContentEncoding::Brotli.to_string(), diff --git a/rust/signed_doc/tests/signature.rs b/rust/signed_doc/tests/signature.rs index 5c93ec25bb..ed7ac1faf6 100644 --- a/rust/signed_doc/tests/signature.rs +++ b/rust/signed_doc/tests/signature.rs @@ -46,7 +46,7 @@ async fn multiple_signatures_validation_test() { let (sk3, pk3, kid3) = common::create_dummy_key_pair(RoleId::Role0).unwrap(); let (_, pk_n, kid_n) = common::create_dummy_key_pair(RoleId::Role0).unwrap(); - let signed_doc = Builder::new() + let signed_doc = CoseSignBuilder::new() .with_decoded_content(serde_json::to_vec(&serde_json::Value::Null).unwrap()) .with_json_metadata(common::test_metadata().2) .unwrap() From 53cb828c47f9b070d207bc5118fa71d798cc9400 Mon Sep 17 00:00:00 2001 From: no30bit Date: Thu, 29 May 2025 13:51:17 +0300 Subject: [PATCH 09/16] move to mod.rs --- rust/signed_doc/src/builder/cbor_map.rs | 49 ----------------- .../{builder/cose.rs => cose_sign/helpers.rs} | 40 +++++++------- .../src/{builder.rs => cose_sign/mod.rs} | 52 ++++++++++--------- rust/signed_doc/src/lib.rs | 4 +- 4 files changed, 48 insertions(+), 97 deletions(-) delete mode 100644 rust/signed_doc/src/builder/cbor_map.rs rename rust/signed_doc/src/{builder/cose.rs => cose_sign/helpers.rs} (68%) rename rust/signed_doc/src/{builder.rs => cose_sign/mod.rs} (74%) diff --git a/rust/signed_doc/src/builder/cbor_map.rs b/rust/signed_doc/src/builder/cbor_map.rs deleted file mode 100644 index 1ea73f7fce..0000000000 --- a/rust/signed_doc/src/builder/cbor_map.rs +++ /dev/null @@ -1,49 +0,0 @@ -use std::collections::BTreeMap; - -use super::EncodeError; - -/// A map of CBOR encoded key-value pairs with **bytewise** lexicographic key ordering. -#[derive(Debug, Default)] -pub struct CborMap(BTreeMap, Vec>); - -impl CborMap { - /// Creates an empty map. - #[must_use] - pub fn new() -> Self { - Self::default() - } - - /// A number of entries in a map. - pub fn len(&self) -> usize { - self.0.len() - } - - /// Is there no entries in the map. - pub fn is_empty(&self) -> bool { - self.len() == 0 - } - - /// Encodes a key-value pair to CBOR and then inserts it into the map. - /// - /// If the map did not have this key present, [`None`] is returned. - /// - /// If the map did have this key present, the value is updated, and the old - /// CBOR-encoded value is returned. - pub fn encode_and_insert, V: minicbor::Encode>( - &mut self, ctx: &mut C, key: K, v: V, - ) -> Result>, EncodeError> { - let (encoded_key, encoded_v) = ( - minicbor::to_vec_with(key, ctx)?, - minicbor::to_vec_with(v, ctx)?, - ); - Ok(self.0.insert(encoded_key, encoded_v)) - } - - /// Iterate over CBOR-encoded key-value pairs. - /// Items are returned in **bytewise** lexicographic key ordering. - pub fn iter(&self) -> impl Iterator { - self.0 - .iter() - .map(|(key_vec, value_vec)| (key_vec.as_slice(), value_vec.as_slice())) - } -} diff --git a/rust/signed_doc/src/builder/cose.rs b/rust/signed_doc/src/cose_sign/helpers.rs similarity index 68% rename from rust/signed_doc/src/builder/cose.rs rename to rust/signed_doc/src/cose_sign/helpers.rs index b81f8df062..838c9caf21 100644 --- a/rust/signed_doc/src/builder/cose.rs +++ b/rust/signed_doc/src/cose_sign/helpers.rs @@ -4,18 +4,17 @@ use minicbor::{ Encode as _, }; -use super::EncodeError; +use super::VecEncodeError; -/// Make the header using the provided cbor-encoded key-value pairs representing -/// fields, conforming to the header fields specification -/// as per [RFC 8152](https://datatracker.ietf.org/doc/html/rfc8152#autoid-8). -pub fn make_header<'a, I>(encoded_fields: I) -> Vec +/// Encode headers using the provided cbor-encoded key-value pairs, +/// conforming to the [RFC 8152 specification](https://datatracker.ietf.org/doc/html/rfc8152#autoid-8). +pub fn encode_headers<'a, I>(iter: I) -> Vec where I: IntoIterator, { let mut encoder = minicbor::Encoder::new(vec![]); - let iter = encoded_fields.into_iter(); + let iter = iter.into_iter(); let map_len = u64::try_from(iter.len()).unwrap_or(u64::MAX); encoder.map(map_len); @@ -28,12 +27,12 @@ where encoder.into_writer() } -/// Make the protected header for the `Cose_signature`, conforming to the header fields specification. +/// Encode a single protected `kid` header for the COSE Signature. /// /// # Errors /// /// - If encoding of the `kid` fails. -pub fn make_signature_header(kid: &[u8]) -> Result, EncodeError> { +pub fn encoed_kid_header(kid: &[u8]) -> Result, VecEncodeError> { /// The KID label as per [RFC 8152 3.1 section](https://datatracker.ietf.org/doc/html/rfc8152#section-3.1). pub const KID_LABEL: u8 = 4; @@ -43,39 +42,38 @@ pub fn make_signature_header(kid: &[u8]) -> Result, EncodeError> { Ok(encoder.into_writer()) } -/// Create a binary blob that will be signed and construct the to-be-signed data from it -/// in-place. Specialized for Catalyst Signed Document (e.g. no support for unprotected headers). +/// Create a binary blob that will be signed. No support for unprotected headers. /// /// Described in [section 2 of RFC 8152](https://datatracker.ietf.org/doc/html/rfc8152#section-2). -pub fn make_tbs_data( - metadata_header: &[u8], signature_header: &[u8], content: Option<&[u8]>, -) -> Result, EncodeError> { +pub fn encode_tbs_data( + protected_headers: &[u8], signature_header: &[u8], content: Option<&[u8]>, +) -> Result, VecEncodeError> { /// The context string as per [RFC 8152 section 4.4](https://datatracker.ietf.org/doc/html/rfc8152#section-4.4). const SIGNATURE_CONTEXT: &str = "Signature"; minicbor::to_vec(( SIGNATURE_CONTEXT, - <&ByteSlice>::from(metadata_header), + <&ByteSlice>::from(protected_headers), <&ByteSlice>::from(signature_header), ByteArray::from([]), // no aad. <&ByteSlice>::from(content.unwrap_or(&[])), // allowing no payload (i.e. no content). )) } -/// Make `Cose_signature`. +/// Encode COSE signature. /// /// Signature bytes should represent a cryptographic signature. -pub fn make_cose_signature( +pub fn encode_cose_signature( protected_header: &[u8], signature_bytes: &[u8], -) -> Result, EncodeError> { +) -> Result, VecEncodeError> { minicbor::to_vec([ <&ByteSlice>::from(protected_header), <&ByteSlice>::from(signature_bytes), ]) } -/// Collect an array from an iterator of pre-encoded `Cose_signature` items. -fn collect_cose_signature_array(signatures: S) -> Result, EncodeError> +/// Encode an array from an iterator of pre-encoded COSE Signature items. +fn encode_cose_signature_array(signatures: S) -> Result, VecEncodeError> where S: IntoIterator, IntoIter: ExactSizeIterator>, { @@ -89,7 +87,7 @@ where Ok(encoder.into_writer()) } -/// Make cbor-encoded tagged `Cose_Sign`. +/// Make cbor-encoded tagged [RFC9052-CoseSign](https://datatracker.ietf.org/doc/html/rfc9052). pub fn encode_cose_sign( e: &mut minicbor::encode::Encoder, protected: &[u8], payload: Option<&[u8]>, signatures: S, ) -> Result<(), minicbor::encode::Error> @@ -103,7 +101,7 @@ where <&ByteSlice>::from(protected), ByteArray::from([]), // unprotected. payload.map(<&ByteSlice>::from), // allowing `NULL`. - collect_cose_signature_array(signatures).map_err(minicbor::encode::Error::custom)?, + encode_cose_signature_array(signatures).map_err(minicbor::encode::Error::custom)?, )); tagged_array.encode(e, &mut ()) } diff --git a/rust/signed_doc/src/builder.rs b/rust/signed_doc/src/cose_sign/mod.rs similarity index 74% rename from rust/signed_doc/src/builder.rs rename to rust/signed_doc/src/cose_sign/mod.rs index 445d6600ce..a6e82ca594 100644 --- a/rust/signed_doc/src/builder.rs +++ b/rust/signed_doc/src/cose_sign/mod.rs @@ -1,22 +1,20 @@ //! Catalyst Signed Document Builder. -/// An implementation of [`CborMap`]. -mod cbor_map; -/// COSE format utils. -mod cose; +/// Encoding helpers. +mod helpers; use std::{convert::Infallible, fmt::Debug, sync::Arc}; use catalyst_types::catalyst_id::CatalystId; -use cose::{ - encode_cose_sign, make_cose_signature, make_header, make_signature_header, make_tbs_data, +use helpers::{ + encode_cose_sign, encode_cose_signature, encode_headers, encode_tbs_data, encoed_kid_header, }; use indexmap::IndexMap; -pub type EncodeError = minicbor::encode::Error; +pub type VecEncodeError = minicbor::encode::Error; -/// [RFC9052-CoseSign] builder. +/// [RFC9052-CoseSign] builder without unprotected fields. /// /// [RFC9052-CoseSign]: https://datatracker.ietf.org/doc/html/rfc9052#name-signing-with-one-or-more-si #[derive(Debug, Default)] @@ -28,7 +26,7 @@ pub struct CoseSignBuilder { } impl CoseSignBuilder { - /// Start building a signed document. + /// Start building a [`CoseSign`]. #[must_use] pub fn new() -> Self { Self::default() @@ -45,7 +43,7 @@ impl CoseSignBuilder { self } - /// Add a field to the protected header. + /// Add a protected header. /// /// If the key is already present, the value is updated. /// @@ -53,9 +51,9 @@ impl CoseSignBuilder { /// /// - Fails if it the CBOR encoding fails. /// - Fails if the key is already present. - pub fn add_protected_field( + pub fn add_protected_header( &mut self, ctx: &mut C, key: K, v: V, - ) -> Result<&mut Self, EncodeError> + ) -> Result<&mut Self, VecEncodeError> where K: minicbor::Encode + Debug, V: minicbor::Encode, @@ -65,8 +63,8 @@ impl CoseSignBuilder { minicbor::to_vec_with(v, ctx)?, ); let indexmap::map::Entry::Vacant(entry) = self.protected.entry(encoded_key) else { - return Err(EncodeError::message(format!( - "Trying to build a CoseSign with duplicate metadata keys (key: {key:?})" + return Err(VecEncodeError::message(format!( + "Trying to build a CoseSign with duplicate protected keys (key: {key:?})" ))); }; entry.insert(encoded_v); @@ -81,10 +79,10 @@ impl CoseSignBuilder { .protected .iter() .map(|(key, v)| (key.as_slice(), v.as_slice())); - make_header(metadata_fields) + encode_headers(metadata_fields) } - fn signer(&self) -> CoseSign { + fn to_cose_sign(&self) -> CoseSign { let protected = self.encode_protected_header(); CoseSign { protected, @@ -100,13 +98,11 @@ impl CoseSignBuilder { /// /// # Errors /// - /// Fails if a `CatalystSignedDocument` cannot be created due to missing metadata or - /// content, due to malformed data, or when the signed document cannot be - /// converted into `coset::CoseSign`. + /// - If CBOR encoding of the [`CatalystId`] fails. pub fn add_signature) -> Vec>( &self, kid: CatalystId, sign_fn: F, - ) -> Result { - let mut signer = self.signer(); + ) -> Result { + let mut signer = self.to_cose_sign(); signer.add_signature(kid, sign_fn)?; Ok(signer) } @@ -123,6 +119,11 @@ pub struct CoseSign { } impl CoseSign { + /// Start building a [`CoseSign`]. Shortcut for the [`CoseSignBuilder::new`]. + pub fn builder() -> CoseSignBuilder { + CoseSignBuilder::new() + } + /// Add another signature to the [`CoseSign`]. /// /// # Errors @@ -130,15 +131,16 @@ impl CoseSign { /// - If CBOR encoding of the [`CatalystId`] fails. pub fn add_signature) -> Vec>( &mut self, kid: CatalystId, sign_fn: F, - ) -> Result<&mut Self, EncodeError> { + ) -> Result<&mut Self, VecEncodeError> { let kid_str = kid.to_string().into_bytes(); - let signature_header = make_signature_header(kid_str.as_slice())?; + let signature_header = encoed_kid_header(kid_str.as_slice())?; - let tbs_data = make_tbs_data(&self.protected, &signature_header, self.payload.as_deref())?; + let tbs_data = + encode_tbs_data(&self.protected, &signature_header, self.payload.as_deref())?; let signature_bytes = sign_fn(tbs_data); // This shouldn't fail. - let signature = make_cose_signature(&signature_header, &signature_bytes)?; + let signature = encode_cose_signature(&signature_header, &signature_bytes)?; self.signatures.push(signature); Ok(self) diff --git a/rust/signed_doc/src/lib.rs b/rust/signed_doc/src/lib.rs index 772aaa0299..104e394e7d 100644 --- a/rust/signed_doc/src/lib.rs +++ b/rust/signed_doc/src/lib.rs @@ -1,6 +1,6 @@ //! Catalyst documents signing crate -mod builder; +mod cose_sign; mod content; mod decode_context; pub mod doc_types; @@ -16,7 +16,7 @@ use std::{ }; use anyhow::Context; -pub use builder::CoseSignBuilder; +pub use cose_sign::CoseSignBuilder; pub use catalyst_types::{ problem_report::ProblemReport, uuid::{Uuid, UuidV4, UuidV7}, From 63208f7c5c92eef029becc7c466f67dc7742eaf4 Mon Sep 17 00:00:00 2001 From: no30bit Date: Thu, 29 May 2025 13:59:26 +0300 Subject: [PATCH 10/16] rm dead code --- rust/cbork-utils/src/decode_helper.rs | 53 ++------------------ rust/signed_doc/Cargo.toml | 5 +- rust/signed_doc/src/metadata/content_type.rs | 1 - 3 files changed, 5 insertions(+), 54 deletions(-) diff --git a/rust/cbork-utils/src/decode_helper.rs b/rust/cbork-utils/src/decode_helper.rs index 88d87f974c..5238c5295c 100644 --- a/rust/cbork-utils/src/decode_helper.rs +++ b/rust/cbork-utils/src/decode_helper.rs @@ -10,7 +10,9 @@ use minicbor::{data::Tag, decode, Decoder}; pub fn decode_helper<'a, T, C>( d: &mut Decoder<'a>, from: &str, context: &mut C, ) -> Result -where T: minicbor::Decode<'a, C> { +where + T: minicbor::Decode<'a, C>, +{ T::decode(d, context).map_err(|e| { decode::Error::message(format!( "Failed to decode {:?} in {from}: {e}", @@ -19,26 +21,6 @@ where T: minicbor::Decode<'a, C> { }) } -/// Generic helper function for decoding different types. -/// -/// # Errors -/// -/// Error if the decoding fails. -pub fn decode_to_end_helper<'a, T, C>( - d: &mut Decoder<'a>, from: &str, context: &mut C, -) -> Result -where T: minicbor::Decode<'a, C> { - let decoded = decode_helper(d, from, context)?; - if d.position() == d.input().len() { - Ok(decoded) - } else { - Err(decode::Error::message(format!( - "Unused bytes remain in the input after decoding {:?} in {from}", - std::any::type_name::() - ))) - } -} - /// Helper function for decoding bytes. /// /// # Errors @@ -95,9 +77,6 @@ pub fn decode_tag(d: &mut Decoder, from: &str) -> Result { } /// Decode any in CDDL (any CBOR type) and return its bytes. -/// This function **allows** unused remainder bytes, unlike [`decode_any_to_end`]. -/// Unless an element of the [RFC 8742 CBOR Sequence](https://datatracker.ietf.org/doc/rfc8742/) -/// is expected to be decoded, the use of this function might cause invalid input to pass. /// /// # Errors /// @@ -115,24 +94,6 @@ pub fn decode_any<'d>(d: &mut Decoder<'d>, from: &str) -> Result<&'d [u8], decod Ok(bytes) } -/// Decode any in CDDL (any CBOR type) and return its bytes. This function guarantees that -/// no unused bytes remain in the [`Decoder`]. If unused remainder is expected, use -/// [`decode_any`]. -/// -/// # Errors -/// -/// Error if the decoding fails or if [`Decoder`] is not fully consumed. -pub fn decode_any_to_end<'d>(d: &mut Decoder<'d>, from: &str) -> Result<&'d [u8], decode::Error> { - let decoded = decode_any(d, from)?; - if d.position() == d.input().len() { - Ok(decoded) - } else { - Err(decode::Error::message(format!( - "Unused bytes remain in the input after decoding in {from}" - ))) - } -} - #[cfg(test)] mod tests { use minicbor::Encoder; @@ -218,12 +179,4 @@ mod tests { // Should print out the error message with the location of the error assert!(result.is_err()); } - - #[test] - fn test_decode_any_seq() { - let mut d = Decoder::new(&[]); - let result = decode_any(&mut d, "test"); - // Should print out the error message with the location of the error - assert!(result.is_err()); - } } diff --git a/rust/signed_doc/Cargo.toml b/rust/signed_doc/Cargo.toml index 8d12e5e72f..67784dcc06 100644 --- a/rust/signed_doc/Cargo.toml +++ b/rust/signed_doc/Cargo.toml @@ -12,13 +12,12 @@ workspace = true [dependencies] catalyst-types = { version = "0.0.3", path = "../catalyst-types" } -cbork-utils = { version = "0.0.1", path = "../cbork-utils" } anyhow = "1.0.95" serde = { version = "1.0.217", features = ["derive"] } -serde_json = { version = "1.0.134", features = ["raw_value"] } +serde_json = "1.0.134" coset = "0.3.8" -minicbor = { version = "0.25.1", features = ["std"] } +minicbor = { version = "0.25.1", features = ["std", "half"] } brotli = "7.0.0" ed25519-dalek = { version = "2.1.1", features = ["rand_core", "pem"] } hex = "0.4.3" diff --git a/rust/signed_doc/src/metadata/content_type.rs b/rust/signed_doc/src/metadata/content_type.rs index 67f72425c4..283c11283d 100644 --- a/rust/signed_doc/src/metadata/content_type.rs +++ b/rust/signed_doc/src/metadata/content_type.rs @@ -1,6 +1,5 @@ //! Document Payload Content Type. -use cbork_utils::decode_helper::decode_any_to_end; use strum::{AsRefStr, Display as EnumDisplay, EnumString, VariantArray}; /// Payload Content Type. From 57f2e232a136483c38deffac5aca704c2572b51d Mon Sep 17 00:00:00 2001 From: no30bit Date: Thu, 29 May 2025 14:05:10 +0300 Subject: [PATCH 11/16] upd Cargo.toml --- .config/dictionaries/project.dic | 1 - rust/signed_doc/Cargo.toml | 5 ++--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.config/dictionaries/project.dic b/.config/dictionaries/project.dic index ce6a8ad4f9..79bec3d502 100644 --- a/.config/dictionaries/project.dic +++ b/.config/dictionaries/project.dic @@ -326,4 +326,3 @@ xprivate XPRV xpub yoroi -bytewise diff --git a/rust/signed_doc/Cargo.toml b/rust/signed_doc/Cargo.toml index 8d12e5e72f..67784dcc06 100644 --- a/rust/signed_doc/Cargo.toml +++ b/rust/signed_doc/Cargo.toml @@ -12,13 +12,12 @@ workspace = true [dependencies] catalyst-types = { version = "0.0.3", path = "../catalyst-types" } -cbork-utils = { version = "0.0.1", path = "../cbork-utils" } anyhow = "1.0.95" serde = { version = "1.0.217", features = ["derive"] } -serde_json = { version = "1.0.134", features = ["raw_value"] } +serde_json = "1.0.134" coset = "0.3.8" -minicbor = { version = "0.25.1", features = ["std"] } +minicbor = { version = "0.25.1", features = ["std", "half"] } brotli = "7.0.0" ed25519-dalek = { version = "2.1.1", features = ["rand_core", "pem"] } hex = "0.4.3" From fcd6b54391965d393040e4acf0ab01450cac1f69 Mon Sep 17 00:00:00 2001 From: no30bit Date: Thu, 29 May 2025 18:01:36 +0300 Subject: [PATCH 12/16] wip --- rust/catalyst-types/src/uuid/uuid_v7.rs | 6 +- rust/signed_doc/Cargo.toml | 3 +- rust/signed_doc/src/cose_sign/mod.rs | 16 ++++ rust/signed_doc/src/lib.rs | 20 +++-- .../src/metadata/content_encoding.rs | 32 +++++++- rust/signed_doc/src/metadata/content_type.rs | 47 ++++++++++-- rust/signed_doc/src/metadata/document_ref.rs | 12 ++- rust/signed_doc/src/metadata/extra_fields.rs | 58 ++++++--------- rust/signed_doc/src/metadata/mod.rs | 74 ++++++++++--------- rust/signed_doc/src/metadata/section.rs | 37 ++++++++-- rust/signed_doc/src/metadata/utils.rs | 38 +++++++++- 11 files changed, 237 insertions(+), 106 deletions(-) diff --git a/rust/catalyst-types/src/uuid/uuid_v7.rs b/rust/catalyst-types/src/uuid/uuid_v7.rs index 98fbd8cda6..736c0ada29 100644 --- a/rust/catalyst-types/src/uuid/uuid_v7.rs +++ b/rust/catalyst-types/src/uuid/uuid_v7.rs @@ -7,7 +7,7 @@ use uuid::Uuid; use super::{decode_cbor_uuid, encode_cbor_uuid, CborContext, UuidError, INVALID_UUID}; /// Type representing a `UUIDv7`. -#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, serde::Serialize)] +#[derive(Copy, Clone, PartialEq, Debug, PartialOrd, serde::Serialize)] pub struct UuidV7(Uuid); impl UuidV7 { @@ -96,7 +96,9 @@ impl From for Uuid { impl<'de> serde::Deserialize<'de> for UuidV7 { fn deserialize(deserializer: D) -> Result - where D: serde::Deserializer<'de> { + where + D: serde::Deserializer<'de>, + { let uuid = Uuid::deserialize(deserializer)?; if is_valid(&uuid) { Ok(Self(uuid)) diff --git a/rust/signed_doc/Cargo.toml b/rust/signed_doc/Cargo.toml index 67784dcc06..b6a8c8f06a 100644 --- a/rust/signed_doc/Cargo.toml +++ b/rust/signed_doc/Cargo.toml @@ -12,11 +12,12 @@ workspace = true [dependencies] catalyst-types = { version = "0.0.3", path = "../catalyst-types" } +cbork-utils = { version = "0.0.1", path = "../cbork-utils" } anyhow = "1.0.95" serde = { version = "1.0.217", features = ["derive"] } serde_json = "1.0.134" -coset = "0.3.8" +coset = { version = "0.3.8", features = ["std"] } minicbor = { version = "0.25.1", features = ["std", "half"] } brotli = "7.0.0" ed25519-dalek = { version = "2.1.1", features = ["rand_core", "pem"] } diff --git a/rust/signed_doc/src/cose_sign/mod.rs b/rust/signed_doc/src/cose_sign/mod.rs index a6e82ca594..73f4470da4 100644 --- a/rust/signed_doc/src/cose_sign/mod.rs +++ b/rust/signed_doc/src/cose_sign/mod.rs @@ -71,6 +71,22 @@ impl CoseSignBuilder { Ok(self) } + /// Wraps around [`Self::add_protected_header`]. + /// Doesn't add fields with values equal to [`Default::default`]. + pub fn add_protected_header_if_not_default( + &mut self, ctx: &mut C, key: K, v: V, + ) -> Result<&mut Self, VecEncodeError> + where + K: minicbor::Encode + Debug, + V: minicbor::Encode + Default + PartialEq, + { + if V::default() == v { + Ok(self) + } else { + self.add_protected_header(ctx, key, v) + } + } + /// Encode [`Self::metadata`] by [`make_metadata_header`] with fields in insertion order. // Question: maybe this should be cached (e.g. frozen once filled)? fn encode_protected_header(&self) -> Vec { diff --git a/rust/signed_doc/src/lib.rs b/rust/signed_doc/src/lib.rs index 104e394e7d..61dc316eda 100644 --- a/rust/signed_doc/src/lib.rs +++ b/rust/signed_doc/src/lib.rs @@ -1,7 +1,7 @@ //! Catalyst documents signing crate -mod cose_sign; mod content; +mod cose_sign; mod decode_context; pub mod doc_types; mod metadata; @@ -16,12 +16,13 @@ use std::{ }; use anyhow::Context; -pub use cose_sign::CoseSignBuilder; pub use catalyst_types::{ problem_report::ProblemReport, uuid::{Uuid, UuidV4, UuidV7}, }; pub use content::Content; +use cose_sign::CoseSign; +pub use cose_sign::CoseSignBuilder; use coset::{CborSerializable, Header, TaggedCborSerializable}; pub use metadata::{ ContentEncoding, ContentType, DocType, DocumentRef, ExtraFields, Metadata, Section, @@ -45,10 +46,10 @@ struct InnerCatalystSignedDocument { /// the other validation errors report: ProblemReport, - /// raw CBOR bytes of the `CatalystSignedDocument` object. + /// Raw CBOR bytes of the `CatalystSignedDocument` object. /// It is important to keep them to have a consistency what comes from the decoding /// process, so we would return the same data again - raw_bytes: Option>, + raw_bytes: Vec, } /// Keep all the contents private. @@ -185,6 +186,8 @@ impl CatalystSignedDocument { /// data. #[must_use] pub fn into_builder(&self) -> CoseSignBuilder { + let mut builder = CoseSign::builder(); + // TOOD! self.into() } } @@ -254,7 +257,7 @@ impl Decode<'_, ()> for CatalystSignedDocument { content, signatures, report, - raw_bytes: Some(cose_bytes.to_vec()), + raw_bytes: cose_bytes.to_vec(), } .into()) } @@ -264,13 +267,8 @@ impl Encode<()> for CatalystSignedDocument { fn encode( &self, e: &mut encode::Encoder, _ctx: &mut (), ) -> Result<(), encode::Error> { - let cose_sign = self.as_cose_sign().map_err(encode::Error::message)?; - let cose_bytes = cose_sign.to_tagged_vec().map_err(|e| { - minicbor::encode::Error::message(format!("Failed to encode COSE Sign document: {e}")) - })?; - e.writer_mut() - .write_all(&cose_bytes) + .write_all(&self.inner.raw_bytes) .map_err(|_| minicbor::encode::Error::message("Failed to encode to CBOR")) } } diff --git a/rust/signed_doc/src/metadata/content_encoding.rs b/rust/signed_doc/src/metadata/content_encoding.rs index 2c6d194fdd..afff0356ce 100644 --- a/rust/signed_doc/src/metadata/content_encoding.rs +++ b/rust/signed_doc/src/metadata/content_encoding.rs @@ -1,10 +1,26 @@ //! Document Payload Content Encoding. -use strum::{AsRefStr, Display as EnumDisplay, EnumString, VariantArray}; +use serde::{Deserialize, Serialize}; +use strum::{Display, EnumString, IntoStaticStr, VariantArray}; + +use super::utils::transcode_ciborium_with; /// IANA `CoAP` Content Encoding. -#[derive(Copy, Clone, Debug, PartialEq, Eq, VariantArray, EnumDisplay, EnumString, AsRefStr)] // TODO: add custom parse error type when the [strum issue]([`issue`](https://github.com/Peternator7/strum/issues/430)) fix is merged. +#[derive( + Copy, + Clone, + Debug, + PartialEq, + Eq, + VariantArray, + EnumString, + Display, + IntoStaticStr, + Serialize, + Deserialize, +)] +#[serde(try_from = "&str", into = "&str")] pub enum ContentEncoding { /// Brotli compression.format. #[strum(to_string = "br")] @@ -47,7 +63,7 @@ impl ContentEncoding { "Unsupported Content Type {input:?}, Supported only: {:?}", ContentEncoding::VARIANTS .iter() - .map(AsRef::as_ref) + .map(<&str>::from) .collect::>() )) } @@ -57,7 +73,7 @@ impl minicbor::Encode for ContentEncoding { fn encode( &self, e: &mut minicbor::Encoder, _: &mut C, ) -> Result<(), minicbor::encode::Error> { - e.str(self.as_ref())?.ok() + e.str(<&str>::from(self))?.ok() } } @@ -67,3 +83,11 @@ impl<'b, C> minicbor::Decode<'b, C> for ContentEncoding { s.parse().map_err(|_| Self::decode_error(s)) } } + +impl TryFrom<&coset::cbor::Value> for ContentEncoding { + type Error = minicbor::decode::Error; + + fn try_from(val: &coset::cbor::Value) -> Result { + transcode_ciborium_with(val, &mut ()) + } +} diff --git a/rust/signed_doc/src/metadata/content_type.rs b/rust/signed_doc/src/metadata/content_type.rs index 283c11283d..075af84845 100644 --- a/rust/signed_doc/src/metadata/content_type.rs +++ b/rust/signed_doc/src/metadata/content_type.rs @@ -1,10 +1,26 @@ //! Document Payload Content Type. -use strum::{AsRefStr, Display as EnumDisplay, EnumString, VariantArray}; +use serde::{Deserialize, Serialize}; +use strum::{Display as EnumDisplay, EnumString, IntoStaticStr, VariantArray}; + +use super::utils::{transcode_ciborium_with, transcode_coset_with}; /// Payload Content Type. // TODO: add custom parse error type when the [strum issue]([`issue`](https://github.com/Peternator7/strum/issues/430)) fix is merged. -#[derive(Debug, Copy, Clone, PartialEq, Eq, VariantArray, EnumString, EnumDisplay, AsRefStr)] +#[derive( + Debug, + Copy, + Clone, + PartialEq, + Eq, + VariantArray, + EnumString, + EnumDisplay, + IntoStaticStr, + Serialize, + Deserialize, +)] +#[serde(try_from = "&str", into = "&str")] pub enum ContentType { /// 'application/cbor' #[strum(to_string = "application/cbor")] @@ -19,14 +35,12 @@ impl ContentType { pub(crate) fn validate(self, content: &[u8]) -> anyhow::Result<()> { match self { Self::Json => { - if let Err(e) = serde_json::from_slice::<&serde_json::value::RawValue>(content) { + if let Err(e) = serde_json::from_slice::(content) { anyhow::bail!("Invalid {self} content: {e}") } }, Self::Cbor => { - if let Err(e) = - decode_any_to_end(&mut minicbor::Decoder::new(content), "signed doc content") - { + if let Err(e) = minicbor::decode::(content) { anyhow::bail!("Invalid {self} content: {e}") } }, @@ -40,7 +54,7 @@ impl ContentType { "Unsupported Content Type {input:?}, Supported only: {:?}", ContentType::VARIANTS .iter() - .map(AsRef::as_ref) + .map(<&str>::from) .collect::>() )) } @@ -50,7 +64,7 @@ impl minicbor::Encode for ContentType { fn encode( &self, e: &mut minicbor::Encoder, _: &mut C, ) -> Result<(), minicbor::encode::Error> { - e.str(self.as_ref())?.ok() + e.str(<&str>::from(self))?.ok() } } @@ -61,6 +75,23 @@ impl<'b, C> minicbor::Decode<'b, C> for ContentType { } } +impl TryFrom<&coset::cbor::Value> for ContentType { + type Error = minicbor::decode::Error; + + fn try_from(val: &coset::cbor::Value) -> Result { + transcode_ciborium_with(val, &mut ()) + } +} + +type CosetLabel = coset::RegisteredLabel; + +impl TryFrom<&CosetLabel> for ContentType { + type Error = minicbor::decode::Error; + + fn try_from(val: &CosetLabel) -> Result { + transcode_coset_with(val.clone(), &mut ()) + } +} #[cfg(test)] mod tests { use std::str::FromStr as _; diff --git a/rust/signed_doc/src/metadata/document_ref.rs b/rust/signed_doc/src/metadata/document_ref.rs index 57d6e3affe..d01f2219e1 100644 --- a/rust/signed_doc/src/metadata/document_ref.rs +++ b/rust/signed_doc/src/metadata/document_ref.rs @@ -5,10 +5,10 @@ use std::fmt::Display; use catalyst_types::uuid::CborContext; use cbork_utils::decode_helper; -use super::UuidV7; +use super::{utils::transcode_ciborium_with, UuidV7}; /// Reference to a Document. -#[derive(Copy, Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] +#[derive(Copy, Clone, PartialEq, Debug, serde::Serialize, serde::Deserialize)] pub struct DocumentRef { /// Reference to the Document Id pub id: UuidV7, @@ -46,3 +46,11 @@ impl minicbor::Decode<'_, CborContext> for DocumentRef { Ok(Self { id, ver }) } } + +impl TryFrom<&coset::cbor::Value> for DocumentRef { + type Error = minicbor::decode::Error; + + fn try_from(val: &coset::cbor::Value) -> Result { + transcode_ciborium_with(val, &mut CborContext::Tagged) + } +} diff --git a/rust/signed_doc/src/metadata/extra_fields.rs b/rust/signed_doc/src/metadata/extra_fields.rs index 1376999d00..1090283ef1 100644 --- a/rust/signed_doc/src/metadata/extra_fields.rs +++ b/rust/signed_doc/src/metadata/extra_fields.rs @@ -1,7 +1,10 @@ //! Catalyst Signed Document Extra Fields. use catalyst_types::problem_report::ProblemReport; -use coset::{cbor::Value, Label, ProtectedHeader}; +use coset::{Label, ProtectedHeader}; +use serde::{Deserialize, Serialize}; + +use crate::cose_sign::VecEncodeError; use super::{ cose_protected_header_find, utils::decode_document_field_from_protected_header, DocumentRef, @@ -30,7 +33,7 @@ const CATEGORY_ID_KEY: &str = "category_id"; /// Extra Metadata Fields. /// /// These values are extracted from the COSE Sign protected header labels. -#[derive(Clone, Default, Debug, PartialEq)] +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] pub struct ExtraFields { /// Reference to the latest document. #[serde(rename = "ref", skip_serializing_if = "Option::is_none")] @@ -89,38 +92,6 @@ impl ExtraFields { self.parameters } - /// Fill the COSE header `ExtraFields` data into the header builder. - pub(super) fn fill_cose_header_fields( - &self, mut builder: coset::HeaderBuilder, - ) -> anyhow::Result { - if let Some(doc_ref) = &self.doc_ref { - builder = builder.text_value(REF_KEY.to_string(), Value::try_from(*doc_ref)?); - } - if let Some(template) = &self.template { - builder = builder.text_value(TEMPLATE_KEY.to_string(), Value::try_from(*template)?); - } - if let Some(reply) = &self.reply { - builder = builder.text_value(REPLY_KEY.to_string(), Value::try_from(*reply)?); - } - - if let Some(section) = &self.section { - builder = builder.text_value(SECTION_KEY.to_string(), Value::from(section.clone())); - } - - if !self.collabs.is_empty() { - builder = builder.text_value( - COLLABS_KEY.to_string(), - Value::Array(self.collabs.iter().cloned().map(Value::Text).collect()), - ); - } - - if let Some(parameters) = &self.parameters { - builder = builder.text_value(PARAMETERS_KEY.to_string(), Value::try_from(*parameters)?); - } - - Ok(builder) - } - /// Converting COSE Protected Header to `ExtraFields`. pub(crate) fn from_protected_header( protected: &ProtectedHeader, error_report: &ProblemReport, @@ -223,6 +194,25 @@ impl ExtraFields { extra } + + /// Add [`Self`] fields to the builder as protected headers. + /// + /// # Errors + /// + /// - If encoding of one of the fields fails, [`crate::CoseSignBuilder`] becomes corrupt and an error is returned + #[allow(const_item_mutation, reason = "expected")] + pub(crate) fn fill_cose_sign_builder<'a>( + &self, uuid_ctx: &mut catalyst_types::uuid::CborContext, + builder: &'a mut crate::CoseSignBuilder, + ) -> Result<&'a mut crate::CoseSignBuilder, VecEncodeError> { + builder.add_protected_header_if_not_default(uuid_ctx, REF_KEY, self.doc_ref())?; + builder.add_protected_header_if_not_default(uuid_ctx, TEMPLATE_KEY, self.template())?; + builder.add_protected_header_if_not_default(uuid_ctx, REPLY_KEY, self.reply())?; + builder.add_protected_header_if_not_default(&mut (), SECTION_KEY, self.section())?; + builder.add_protected_header_if_not_default(&mut (), COLLABS_KEY, self.collabs())?; + builder.add_protected_header_if_not_default(uuid_ctx, PARAMETERS_KEY, self.parameters())?; + builder.add_protected_header_if_not_default(uuid_ctx, PARAMETERS_KEY, self.parameters()) + } } #[cfg(test)] diff --git a/rust/signed_doc/src/metadata/mod.rs b/rust/signed_doc/src/metadata/mod.rs index 479012931e..afd322577a 100644 --- a/rust/signed_doc/src/metadata/mod.rs +++ b/rust/signed_doc/src/metadata/mod.rs @@ -15,7 +15,6 @@ use catalyst_types::{ }; pub use content_encoding::ContentEncoding; pub use content_type::ContentType; -use coset::{cbor::Value, iana::CoapContentFormat}; pub use doc_type::DocType; pub use document_ref::DocumentRef; pub use extra_fields::ExtraFields; @@ -24,6 +23,8 @@ use utils::{ cose_protected_header_find, decode_document_field_from_protected_header, CborUuidV4, CborUuidV7, }; +use crate::cose_sign::VecEncodeError; + /// `content_encoding` field COSE key value const CONTENT_ENCODING_KEY: &str = "Content-Encoding"; /// `doc_type` field COSE key value @@ -138,6 +139,42 @@ impl Metadata { let metadata = InnerMetadata::from_protected_header(protected, report); Self::from_metadata_fields(metadata, report) } + + /// Add [`Self`] fields to the builder as protected headers. + pub(crate) fn fill_cose_sign_builder<'a>( + &self, uuid_ctx: &mut catalyst_types::uuid::CborContext, + builder: &'a mut crate::CoseSignBuilder, + ) -> Result<&'a mut crate::CoseSignBuilder, VecEncodeError> { + /// `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"; + builder.add_protected_header_if_not_default( + &mut (), + CONTENT_ENCODING_KEY, + self.content_encoding(), + )?; + builder.add_protected_header( + uuid_ctx, + TYPE_KEY, + self.doc_type().map_err(VecEncodeError::message)?, + )?; + builder.add_protected_header( + uuid_ctx, + ID_KEY, + self.doc_id().map_err(VecEncodeError::message)?, + )?; + builder.add_protected_header( + uuid_ctx, + VER_KEY, + self.doc_ver().map_err(VecEncodeError::message)?, + )?; + self.extra().fill_cose_sign_builder(uuid_ctx, builder) + } } impl InnerMetadata { @@ -227,38 +264,3 @@ impl Display for Metadata { writeln!(f, "}}") } } - -impl TryFrom<&Metadata> for coset::Header { - type Error = anyhow::Error; - - fn try_from(meta: &Metadata) -> Result { - let mut builder = coset::HeaderBuilder::new() - .content_format(CoapContentFormat::from(meta.content_type()?)); - let mut builder = coset::HeaderBuilder::new(); - - if let Some(content_encoding) = meta.content_encoding() { - builder = builder.text_value( - CONTENT_ENCODING_KEY.to_string(), - format!("{content_encoding}").into(), - ); - } - - builder = builder - .text_value( - TYPE_KEY.to_string(), - Value::try_from(CborUuidV4(meta.doc_type()?))?, - ) - .text_value( - ID_KEY.to_string(), - Value::try_from(CborUuidV7(meta.doc_id()?))?, - ) - .text_value( - VER_KEY.to_string(), - Value::try_from(CborUuidV7(meta.doc_ver()?))?, - ); - - builder = meta.0.extra.fill_cose_header_fields(builder)?; - - Ok(builder.build()) - } -} diff --git a/rust/signed_doc/src/metadata/section.rs b/rust/signed_doc/src/metadata/section.rs index 78a313489c..45d5c07713 100644 --- a/rust/signed_doc/src/metadata/section.rs +++ b/rust/signed_doc/src/metadata/section.rs @@ -2,8 +2,13 @@ use std::{fmt::Display, str::FromStr}; +use serde::{Deserialize, Serialize}; + +use super::utils::transcode_ciborium_with; + /// 'section' field type definition, which is a JSON path string -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(into = "String", try_from = "&str")] pub struct Section(jsonpath_rust::JsonPath); impl Display for Section { @@ -12,11 +17,25 @@ impl Display for Section { } } +impl From
for String { + fn from(value: Section) -> Self { + value.to_string() + } +} + impl FromStr for Section { type Err = jsonpath_rust::JsonPathParserError; fn from_str(s: &str) -> Result { - jsonpath_rust::JsonPath::::from_str(s).map(Self) + s.parse().map(Self) + } +} + +impl TryFrom<&str> for Section { + type Error = jsonpath_rust::JsonPathParserError; + + fn try_from(s: &str) -> Result { + s.parse() } } @@ -24,14 +43,20 @@ impl minicbor::Encode for Section { fn encode( &self, e: &mut minicbor::Encoder, _: &mut C, ) -> Result<(), minicbor::encode::Error> { - e.str(&self.0.to_string())?; - Ok(()) + e.str(&self.0.to_string())?.ok() } } impl<'b, C> minicbor::Decode<'b, C> for Section { fn decode(d: &mut minicbor::Decoder<'b>, _: &mut C) -> Result { - let s = d.str()?; - s.parse().map_err(minicbor::decode::Error::custom) + d.str()?.parse().map_err(minicbor::decode::Error::custom) + } +} + +impl TryFrom<&coset::cbor::Value> for Section { + type Error = minicbor::decode::Error; + + fn try_from(val: &coset::cbor::Value) -> Result { + transcode_ciborium_with(val, &mut ()) } } diff --git a/rust/signed_doc/src/metadata/utils.rs b/rust/signed_doc/src/metadata/utils.rs index 0e54f10c43..6181182fde 100644 --- a/rust/signed_doc/src/metadata/utils.rs +++ b/rust/signed_doc/src/metadata/utils.rs @@ -5,6 +5,8 @@ use catalyst_types::{ uuid::{CborContext, UuidV4, UuidV7}, }; use coset::{CborSerializable, Label, ProtectedHeader}; +use minicbor::decode::Decode; +use serde::Serialize; /// Find a value for a predicate in the protected header. pub(crate) fn cose_protected_header_find( @@ -22,7 +24,9 @@ pub(crate) fn cose_protected_header_find( 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> { +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())) { @@ -91,7 +95,7 @@ fn encode_cbor_uuid>( /// 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>>( +fn decode_cbor_uuid Decode<'a, CborContext>>( value: &coset::cbor::Value, ) -> anyhow::Result { let mut cbor_bytes = Vec::new(); @@ -100,3 +104,33 @@ fn decode_cbor_uuid minicbor::decode::Decode<'a, CborContext>>( minicbor::decode_with(&cbor_bytes, &mut CborContext::Tagged) .map_err(|e| anyhow::anyhow!("Invalid UUID, err: {e}")) } + +/// Transcode [`ciborium`](coset::cbor) to a type implementing [`minicbor::Decode`]. +/// +/// This is rather inefficient, but allows to keep a single CBOR implementation. +pub(crate) fn transcode_ciborium_with( + value: &T, ctx: &mut C, +) -> Result +where + T: Serialize, + U: for<'a> Decode<'a, C>, +{ + let mut cbor_bytes = Vec::new(); + coset::cbor::ser::into_writer(value, &mut cbor_bytes) + .map_err(minicbor::decode::Error::custom)?; + minicbor::decode_with(&cbor_bytes, ctx) +} + +/// Transcode [`coset::CborSerializable`] to a type implementing [`minicbor::Decode`]. +/// +/// This is rather inefficient, but allows to keep a single CBOR implementation. +pub(crate) fn transcode_coset_with( + value: T, ctx: &mut C, +) -> Result +where + T: coset::CborSerializable, + U: for<'a> Decode<'a, C>, +{ + let cbor_bytes = value.to_vec().map_err(minicbor::decode::Error::custom)?; + minicbor::decode_with(&cbor_bytes, ctx) +} From d397a3ad73c86816fa6d871f1290404d858081a9 Mon Sep 17 00:00:00 2001 From: no30bit Date: Thu, 29 May 2025 18:02:49 +0300 Subject: [PATCH 13/16] fmt --- rust/catalyst-types/src/uuid/uuid_v7.rs | 6 ++---- rust/cbork-utils/src/decode_helper.rs | 4 +--- rust/signed_doc/bins/mk_signed_doc.rs | 2 +- rust/signed_doc/src/cose_sign/helpers.rs | 12 +++--------- rust/signed_doc/src/cose_sign/mod.rs | 8 +++----- rust/signed_doc/src/metadata/extra_fields.rs | 6 +++--- rust/signed_doc/src/metadata/utils.rs | 4 +--- rust/signed_doc/src/validator/rules/signature_kid.rs | 2 +- 8 files changed, 15 insertions(+), 29 deletions(-) diff --git a/rust/catalyst-types/src/uuid/uuid_v7.rs b/rust/catalyst-types/src/uuid/uuid_v7.rs index 736c0ada29..98fbd8cda6 100644 --- a/rust/catalyst-types/src/uuid/uuid_v7.rs +++ b/rust/catalyst-types/src/uuid/uuid_v7.rs @@ -7,7 +7,7 @@ use uuid::Uuid; use super::{decode_cbor_uuid, encode_cbor_uuid, CborContext, UuidError, INVALID_UUID}; /// Type representing a `UUIDv7`. -#[derive(Copy, Clone, PartialEq, Debug, PartialOrd, serde::Serialize)] +#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, serde::Serialize)] pub struct UuidV7(Uuid); impl UuidV7 { @@ -96,9 +96,7 @@ impl From for Uuid { impl<'de> serde::Deserialize<'de> for UuidV7 { fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { + where D: serde::Deserializer<'de> { let uuid = Uuid::deserialize(deserializer)?; if is_valid(&uuid) { Ok(Self(uuid)) diff --git a/rust/cbork-utils/src/decode_helper.rs b/rust/cbork-utils/src/decode_helper.rs index 5238c5295c..1a8ab480f1 100644 --- a/rust/cbork-utils/src/decode_helper.rs +++ b/rust/cbork-utils/src/decode_helper.rs @@ -10,9 +10,7 @@ use minicbor::{data::Tag, decode, Decoder}; pub fn decode_helper<'a, T, C>( d: &mut Decoder<'a>, from: &str, context: &mut C, ) -> Result -where - T: minicbor::Decode<'a, C>, -{ +where T: minicbor::Decode<'a, C> { T::decode(d, context).map_err(|e| { decode::Error::message(format!( "Failed to decode {:?} in {from}: {e}", diff --git a/rust/signed_doc/bins/mk_signed_doc.rs b/rust/signed_doc/bins/mk_signed_doc.rs index 7764e8ba5e..4ecdf4de53 100644 --- a/rust/signed_doc/bins/mk_signed_doc.rs +++ b/rust/signed_doc/bins/mk_signed_doc.rs @@ -9,7 +9,7 @@ use std::{ }; use anyhow::Context; -use catalyst_signed_doc::{CoseSignBuilder, CatalystId, CatalystSignedDocument}; +use catalyst_signed_doc::{CatalystId, CatalystSignedDocument, CoseSignBuilder}; use clap::Parser; fn main() { diff --git a/rust/signed_doc/src/cose_sign/helpers.rs b/rust/signed_doc/src/cose_sign/helpers.rs index 838c9caf21..a5f849942a 100644 --- a/rust/signed_doc/src/cose_sign/helpers.rs +++ b/rust/signed_doc/src/cose_sign/helpers.rs @@ -9,9 +9,7 @@ use super::VecEncodeError; /// Encode headers using the provided cbor-encoded key-value pairs, /// conforming to the [RFC 8152 specification](https://datatracker.ietf.org/doc/html/rfc8152#autoid-8). pub fn encode_headers<'a, I>(iter: I) -> Vec -where - I: IntoIterator, -{ +where I: IntoIterator { let mut encoder = minicbor::Encoder::new(vec![]); let iter = iter.into_iter(); @@ -74,9 +72,7 @@ pub fn encode_cose_signature( /// Encode an array from an iterator of pre-encoded COSE Signature items. fn encode_cose_signature_array(signatures: S) -> Result, VecEncodeError> -where - S: IntoIterator, IntoIter: ExactSizeIterator>, -{ +where S: IntoIterator, IntoIter: ExactSizeIterator> { let iter = signatures.into_iter(); let array_len = u64::try_from(iter.len().saturating_add(1)).unwrap_or(u64::MAX); let mut encoder = minicbor::Encoder::new(vec![]); @@ -91,9 +87,7 @@ where pub fn encode_cose_sign( e: &mut minicbor::encode::Encoder, protected: &[u8], payload: Option<&[u8]>, signatures: S, ) -> Result<(), minicbor::encode::Error> -where - S: IntoIterator, IntoIter: ExactSizeIterator>, -{ +where S: IntoIterator, IntoIter: ExactSizeIterator> { /// From the table in [section 2 of RFC 8152](https://datatracker.ietf.org/doc/html/rfc8152#section-2). const COSE_SIGN_TAG: u64 = 98; diff --git a/rust/signed_doc/src/cose_sign/mod.rs b/rust/signed_doc/src/cose_sign/mod.rs index 73f4470da4..d4aac1d710 100644 --- a/rust/signed_doc/src/cose_sign/mod.rs +++ b/rust/signed_doc/src/cose_sign/mod.rs @@ -6,7 +6,6 @@ mod helpers; use std::{convert::Infallible, fmt::Debug, sync::Arc}; use catalyst_types::catalyst_id::CatalystId; - use helpers::{ encode_cose_sign, encode_cose_signature, encode_headers, encode_tbs_data, encoed_kid_header, }; @@ -36,9 +35,7 @@ impl CoseSignBuilder { /// encoding algorithm from the `content-encoding` field. #[must_use] pub fn with_payload(&mut self, payload: T) -> &mut Self - where - Arc<[u8]>: From, - { + where Arc<[u8]>: From { self.payload = Some(payload.into()); self } @@ -87,7 +84,8 @@ impl CoseSignBuilder { } } - /// Encode [`Self::metadata`] by [`make_metadata_header`] with fields in insertion order. + /// Encode [`Self::metadata`] by [`make_metadata_header`] with fields in insertion + /// order. // Question: maybe this should be cached (e.g. frozen once filled)? fn encode_protected_header(&self) -> Vec { // This iterates in insertion order. diff --git a/rust/signed_doc/src/metadata/extra_fields.rs b/rust/signed_doc/src/metadata/extra_fields.rs index 1090283ef1..f012789d57 100644 --- a/rust/signed_doc/src/metadata/extra_fields.rs +++ b/rust/signed_doc/src/metadata/extra_fields.rs @@ -4,12 +4,11 @@ use catalyst_types::problem_report::ProblemReport; use coset::{Label, ProtectedHeader}; use serde::{Deserialize, Serialize}; -use crate::cose_sign::VecEncodeError; - use super::{ cose_protected_header_find, utils::decode_document_field_from_protected_header, DocumentRef, Section, }; +use crate::cose_sign::VecEncodeError; /// `ref` field COSE key value const REF_KEY: &str = "ref"; @@ -199,7 +198,8 @@ impl ExtraFields { /// /// # Errors /// - /// - If encoding of one of the fields fails, [`crate::CoseSignBuilder`] becomes corrupt and an error is returned + /// - If encoding of one of the fields fails, [`crate::CoseSignBuilder`] becomes + /// corrupt and an error is returned #[allow(const_item_mutation, reason = "expected")] pub(crate) fn fill_cose_sign_builder<'a>( &self, uuid_ctx: &mut catalyst_types::uuid::CborContext, diff --git a/rust/signed_doc/src/metadata/utils.rs b/rust/signed_doc/src/metadata/utils.rs index 6181182fde..9903847f5c 100644 --- a/rust/signed_doc/src/metadata/utils.rs +++ b/rust/signed_doc/src/metadata/utils.rs @@ -24,9 +24,7 @@ pub(crate) fn cose_protected_header_find( 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>, -{ +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())) { diff --git a/rust/signed_doc/src/validator/rules/signature_kid.rs b/rust/signed_doc/src/validator/rules/signature_kid.rs index afb8b4cf05..e4d111b94a 100644 --- a/rust/signed_doc/src/validator/rules/signature_kid.rs +++ b/rust/signed_doc/src/validator/rules/signature_kid.rs @@ -47,7 +47,7 @@ mod tests { use ed25519_dalek::ed25519::signature::Signer; use super::*; - use crate::{CoseSignBuilder, ContentType}; + use crate::{ContentType, CoseSignBuilder}; #[tokio::test] async fn signature_kid_rule_test() { From 2eb553b971c049d79e3638d501263dad3251b98b Mon Sep 17 00:00:00 2001 From: no30bit Date: Fri, 30 May 2025 10:40:54 +0300 Subject: [PATCH 14/16] encode content type, fixes --- rust/signed_doc/src/lib.rs | 2 +- rust/signed_doc/src/metadata/mod.rs | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/rust/signed_doc/src/lib.rs b/rust/signed_doc/src/lib.rs index 61dc316eda..217a377672 100644 --- a/rust/signed_doc/src/lib.rs +++ b/rust/signed_doc/src/lib.rs @@ -188,7 +188,7 @@ impl CatalystSignedDocument { pub fn into_builder(&self) -> CoseSignBuilder { let mut builder = CoseSign::builder(); // TOOD! - self.into() + todo!() } } diff --git a/rust/signed_doc/src/metadata/mod.rs b/rust/signed_doc/src/metadata/mod.rs index afd322577a..c6d331bd65 100644 --- a/rust/signed_doc/src/metadata/mod.rs +++ b/rust/signed_doc/src/metadata/mod.rs @@ -33,6 +33,8 @@ const TYPE_KEY: &str = "type"; const ID_KEY: &str = "id"; /// `ver` field COSE key value const VER_KEY: &str = "ver"; +/// `content_type` field COSE key value +const CONTENT_TYPE: u8 = 3; /// Document Metadata. /// @@ -153,6 +155,11 @@ impl Metadata { const ID_KEY: &str = "id"; /// `ver` field COSE key value const VER_KEY: &str = "ver"; + builder.add_protected_header( + &mut (), + CONTENT_TYPE, + self.content_type().map_err(VecEncodeError::message)?, + )?; builder.add_protected_header_if_not_default( &mut (), CONTENT_ENCODING_KEY, From e97bd3c25df03a7cfeebc0b69aa5fb9607abc916 Mon Sep 17 00:00:00 2001 From: no30bit Date: Fri, 30 May 2025 10:48:19 +0300 Subject: [PATCH 15/16] remove accidental change --- rust/signed_doc/src/metadata/utils.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/rust/signed_doc/src/metadata/utils.rs b/rust/signed_doc/src/metadata/utils.rs index 9903847f5c..c2d1176b17 100644 --- a/rust/signed_doc/src/metadata/utils.rs +++ b/rust/signed_doc/src/metadata/utils.rs @@ -24,7 +24,9 @@ pub(crate) fn cose_protected_header_find( 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> { +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())) { @@ -93,7 +95,7 @@ fn encode_cbor_uuid>( /// Decode `From` type from `coset::cbor::Value`. /// /// This is used to decode `UuidV4` and `UuidV7` types. -fn decode_cbor_uuid Decode<'a, CborContext>>( +fn decode_cbor_uuid minicbor::decode::Decode<'a, CborContext>>( value: &coset::cbor::Value, ) -> anyhow::Result { let mut cbor_bytes = Vec::new(); @@ -127,7 +129,7 @@ pub(crate) fn transcode_coset_with( ) -> Result where T: coset::CborSerializable, - U: for<'a> Decode<'a, C>, + U: for<'a> minicbor::decode::Decode<'a, C>, { let cbor_bytes = value.to_vec().map_err(minicbor::decode::Error::custom)?; minicbor::decode_with(&cbor_bytes, ctx) From 1515bb7b7f30b5f3e376d13b89d4b7650ab235bc Mon Sep 17 00:00:00 2001 From: no30bit Date: Fri, 30 May 2025 10:49:03 +0300 Subject: [PATCH 16/16] fmt --- rust/signed_doc/src/metadata/utils.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/rust/signed_doc/src/metadata/utils.rs b/rust/signed_doc/src/metadata/utils.rs index c2d1176b17..96dffb5524 100644 --- a/rust/signed_doc/src/metadata/utils.rs +++ b/rust/signed_doc/src/metadata/utils.rs @@ -24,9 +24,7 @@ pub(crate) fn cose_protected_header_find( 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>, -{ +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())) {