Skip to content

Commit 8f23447

Browse files
authored
feat(rust/signed-doc): Catalyst Signed Documents minicbor::Decode impl (#383)
* more tests * add signatures decoding logic * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * fix * wip * remove coset * fix clippy * wip * fix spelling * wip * wip * wip * wip * fix comment * wip * add Collaborators type * wip * wip * fix clippy
1 parent e236d9e commit 8f23447

File tree

19 files changed

+359
-685
lines changed

19 files changed

+359
-685
lines changed

rust/catalyst-types/src/catalyst_id/mod.rs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -676,12 +676,9 @@ impl TryFrom<&[u8]> for CatalystId {
676676
}
677677
}
678678

679-
impl minicbor::Encode<()> for CatalystId {
680-
fn encode<W: minicbor::encode::Write>(
681-
&self, e: &mut minicbor::Encoder<W>, _ctx: &mut (),
682-
) -> Result<(), minicbor::encode::Error<W::Error>> {
683-
e.bytes(self.to_string().into_bytes().as_slice())?;
684-
Ok(())
679+
impl From<&CatalystId> for Vec<u8> {
680+
fn from(value: &CatalystId) -> Self {
681+
value.to_string().into_bytes()
685682
}
686683
}
687684

rust/cbork-utils/src/deterministic_helper.rs

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -98,12 +98,11 @@ impl Ord for MapEntry {
9898
/// - Map keys are not properly sorted (`UnorderedMapKeys`)
9999
/// - Duplicate keys are found (`DuplicateMapKey`)
100100
/// - Map key or value decoding fails (`DecoderError`)
101-
pub fn decode_map_deterministically(d: &mut Decoder) -> Result<Vec<u8>, minicbor::decode::Error> {
101+
pub fn decode_map_deterministically(
102+
d: &mut Decoder,
103+
) -> Result<Vec<MapEntry>, minicbor::decode::Error> {
102104
validate_input_not_empty(d)?;
103105

104-
// Store the starting position BEFORE consuming the map header
105-
let map_start = d.position();
106-
107106
// From RFC 8949 Section 4.2.2:
108107
// "Indefinite-length items must be made definite-length items."
109108
// The specification explicitly prohibits indefinite-length items in
@@ -115,18 +114,14 @@ pub fn decode_map_deterministically(d: &mut Decoder) -> Result<Vec<u8>, minicbor
115114
})?;
116115

117116
let header_end_pos = d.position();
118-
119117
check_map_minimal_length(d, header_end_pos, map_len)?;
120118

121119
// Decode entries to validate them
122120
let entries = decode_map_entries(d, map_len)?;
123121

124122
validate_map_ordering(&entries)?;
125123

126-
// Get the ending position after validation
127-
let map_end = d.position();
128-
129-
get_bytes(d, map_start, map_end)
124+
Ok(entries)
130125
}
131126

132127
/// Extracts the raw bytes of a CBOR map from a decoder based on specified positions.
@@ -501,7 +496,21 @@ mod tests {
501496
let result = decode_map_deterministically(&mut decoder).unwrap();
502497

503498
// Verify we got back exactly the same bytes
504-
assert_eq!(result, valid_map);
499+
500+
assert_eq!(result, vec![
501+
MapEntry {
502+
// Key 1: 2-byte string
503+
key_bytes: vec![0x42, 0x01, 0x02],
504+
// Value 1: 1-byte string
505+
value: vec![0x41, 0x01]
506+
},
507+
MapEntry {
508+
// Key 2: 3-byte string
509+
key_bytes: vec![0x43, 0x01, 0x02, 0x03,],
510+
// Value 2: 1-byte string
511+
value: vec![0x41, 0x02,]
512+
}
513+
]);
505514
}
506515

507516
/// Test cases for lexicographic ordering of map keys as specified in RFC 8949 Section

rust/signed_doc/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@ workspace = true
1212

1313
[dependencies]
1414
catalyst-types = { version = "0.0.3", path = "../catalyst-types" }
15+
cbork-utils = { version = "0.0.1", path = "../cbork-utils" }
1516

1617
anyhow = "1.0.95"
1718
serde = { version = "1.0.217", features = ["derive"] }
1819
serde_json = { version = "1.0.134", features = ["raw_value"] }
19-
coset = "0.3.8"
2020
minicbor = { version = "0.25.1", features = ["half"] }
2121
brotli = "7.0.0"
2222
ed25519-dalek = { version = "2.1.1", features = ["rand_core", "pem"] }

rust/signed_doc/src/content.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,18 @@ impl minicbor::Encode<()> for Content {
3636
Ok(())
3737
}
3838
}
39+
40+
impl minicbor::Decode<'_, ()> for Content {
41+
fn decode(
42+
d: &mut minicbor::Decoder<'_>, _ctx: &mut (),
43+
) -> Result<Self, minicbor::decode::Error> {
44+
let p = d.position();
45+
d.null()
46+
.map(|()| Self(Vec::new()))
47+
// important to use `or_else` so it will lazy evaluated at the time when it is needed
48+
.or_else(|_| {
49+
d.set_position(p);
50+
d.bytes().map(Vec::from).map(Self)
51+
})
52+
}
53+
}

rust/signed_doc/src/lib.rs

Lines changed: 41 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ pub use catalyst_types::{
2121
uuid::{Uuid, UuidV4, UuidV7},
2222
};
2323
pub use content::Content;
24-
use coset::{CborSerializable, TaggedCborSerializable};
2524
use decode_context::{CompatibilityPolicy, DecodeContext};
2625
pub use metadata::{
2726
ContentEncoding, ContentType, DocLocator, DocType, DocumentRef, DocumentRefs, Metadata, Section,
@@ -31,8 +30,8 @@ pub use signature::{CatalystId, Signatures};
3130

3231
use crate::builder::SignaturesBuilder;
3332

34-
/// A problem report content string
35-
const PROBLEM_REPORT_CTX: &str = "Catalyst Signed Document";
33+
/// `COSE_Sign` object CBOR tag <https://datatracker.ietf.org/doc/html/rfc8152#page-8>
34+
const COSE_SIGN_CBOR_TAG: minicbor::data::Tag = minicbor::data::Tag::new(98);
3635

3736
/// Inner type that holds the Catalyst Signed Document with parsing errors.
3837
#[derive(Debug)]
@@ -212,34 +211,51 @@ impl CatalystSignedDocument {
212211

213212
impl Decode<'_, ()> for CatalystSignedDocument {
214213
fn decode(d: &mut Decoder<'_>, _ctx: &mut ()) -> Result<Self, decode::Error> {
215-
let start = d.position();
216-
d.skip()?;
217-
let end = d.position();
218-
let cose_bytes = d
219-
.input()
220-
.get(start..end)
221-
.ok_or(minicbor::decode::Error::end_of_input())?;
222-
223-
let cose_sign = coset::CoseSign::from_tagged_slice(cose_bytes)
224-
.or_else(|_| coset::CoseSign::from_slice(cose_bytes))
225-
.map_err(|e| {
226-
minicbor::decode::Error::message(format!("Invalid COSE Sign document: {e}"))
227-
})?;
228-
229-
let mut report = ProblemReport::new(PROBLEM_REPORT_CTX);
214+
let mut report = ProblemReport::new("Catalyst Signed Document Decoding");
230215
let mut ctx = DecodeContext {
231216
compatibility_policy: CompatibilityPolicy::Accept,
232217
report: &mut report,
233218
};
234-
let metadata = Metadata::from_protected_header(&cose_sign.protected, &mut ctx);
235-
let signatures = Signatures::from_cose_sig_list(&cose_sign.signatures, &report);
219+
let start = d.position();
236220

237-
let content = if let Some(payload) = cose_sign.payload {
238-
payload.into()
221+
if let Ok(tag) = d.tag() {
222+
if tag != COSE_SIGN_CBOR_TAG {
223+
return Err(minicbor::decode::Error::message(format!(
224+
"Must be equal to the COSE_Sign tag value: {COSE_SIGN_CBOR_TAG}"
225+
)));
226+
}
239227
} else {
240-
report.missing_field("COSE Sign Payload", "Missing document content (payload)");
241-
Content::default()
242-
};
228+
d.set_position(start);
229+
}
230+
231+
if !matches!(d.array()?, Some(4)) {
232+
return Err(minicbor::decode::Error::message(
233+
"Must be a definite size array of 4 elements",
234+
));
235+
}
236+
237+
let metadata_bytes = d.bytes()?;
238+
let metadata = Metadata::decode(&mut minicbor::Decoder::new(metadata_bytes), &mut ctx)?;
239+
240+
// empty unprotected headers
241+
let mut map =
242+
cbork_utils::deterministic_helper::decode_map_deterministically(d)?.into_iter();
243+
if map.next().is_some() {
244+
ctx.report.unknown_field(
245+
"unprotected headers",
246+
"non empty unprotected headers",
247+
"COSE unprotected headers must be empty",
248+
);
249+
}
250+
251+
let content = Content::decode(d, &mut ())?;
252+
let signatures = Signatures::decode(d, &mut ctx)?;
253+
254+
let end = d.position();
255+
let cose_bytes = d
256+
.input()
257+
.get(start..end)
258+
.ok_or(minicbor::decode::Error::end_of_input())?;
243259

244260
Ok(InnerCatalystSignedDocument {
245261
metadata,
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
//! Catalyst Signed Document `collabs` field type definition.
2+
3+
use std::ops::Deref;
4+
5+
/// 'collabs' field type definition, which is a JSON path string
6+
#[derive(Clone, Debug, PartialEq)]
7+
pub struct Collaborators(Vec<String>);
8+
9+
impl Deref for Collaborators {
10+
type Target = Vec<String>;
11+
12+
fn deref(&self) -> &Self::Target {
13+
&self.0
14+
}
15+
}
16+
17+
impl minicbor::Encode<()> for Collaborators {
18+
fn encode<W: minicbor::encode::Write>(
19+
&self, e: &mut minicbor::Encoder<W>, _ctx: &mut (),
20+
) -> Result<(), minicbor::encode::Error<W::Error>> {
21+
if !self.0.is_empty() {
22+
e.array(
23+
self.0
24+
.len()
25+
.try_into()
26+
.map_err(minicbor::encode::Error::message)?,
27+
)?;
28+
for c in &self.0 {
29+
e.str(c)?;
30+
}
31+
}
32+
Ok(())
33+
}
34+
}
35+
36+
impl minicbor::Decode<'_, ()> for Collaborators {
37+
fn decode(
38+
d: &mut minicbor::Decoder<'_>, _ctx: &mut (),
39+
) -> Result<Self, minicbor::decode::Error> {
40+
let Some(items) = d.array()? else {
41+
return Err(minicbor::decode::Error::message(
42+
"Must a definite size array",
43+
));
44+
};
45+
let collabs = (0..items)
46+
.map(|_| Ok(d.str()?.to_string()))
47+
.collect::<Result<_, _>>()?;
48+
Ok(Self(collabs))
49+
}
50+
}
51+
52+
impl<'de> serde::Deserialize<'de> for Collaborators {
53+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
54+
where D: serde::Deserializer<'de> {
55+
Ok(Self(Vec::<String>::deserialize(deserializer)?))
56+
}
57+
}

rust/signed_doc/src/metadata/content_encoding.rs

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -72,19 +72,6 @@ impl<'de> Deserialize<'de> for ContentEncoding {
7272
}
7373
}
7474

75-
impl TryFrom<&coset::cbor::Value> for ContentEncoding {
76-
type Error = anyhow::Error;
77-
78-
fn try_from(val: &coset::cbor::Value) -> anyhow::Result<ContentEncoding> {
79-
match val.as_text() {
80-
Some(encoding) => encoding.parse(),
81-
None => {
82-
anyhow::bail!("Expected Content Encoding to be a string");
83-
},
84-
}
85-
}
86-
}
87-
8875
impl minicbor::Encode<()> for ContentEncoding {
8976
fn encode<W: minicbor::encode::Write>(
9077
&self, e: &mut minicbor::Encoder<W>, _ctx: &mut (),
@@ -93,3 +80,11 @@ impl minicbor::Encode<()> for ContentEncoding {
9380
Ok(())
9481
}
9582
}
83+
84+
impl minicbor::Decode<'_, ()> for ContentEncoding {
85+
fn decode(
86+
d: &mut minicbor::Decoder<'_>, _ctx: &mut (),
87+
) -> Result<Self, minicbor::decode::Error> {
88+
d.str()?.parse().map_err(minicbor::decode::Error::message)
89+
}
90+
}

rust/signed_doc/src/metadata/content_type.rs

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ use std::{
55
str::FromStr,
66
};
77

8-
use coset::iana::CoapContentFormat;
98
use serde::{de, Deserialize, Deserializer};
109
use strum::VariantArray;
1110

@@ -55,27 +54,6 @@ impl<'de> Deserialize<'de> for ContentType {
5554
}
5655
}
5756

58-
impl TryFrom<&coset::ContentType> for ContentType {
59-
type Error = anyhow::Error;
60-
61-
fn try_from(value: &coset::ContentType) -> Result<Self, Self::Error> {
62-
match value {
63-
coset::ContentType::Assigned(CoapContentFormat::Json) => Ok(ContentType::Json),
64-
coset::ContentType::Assigned(CoapContentFormat::Cbor) => Ok(ContentType::Cbor),
65-
coset::ContentType::Text(str) => str.parse(),
66-
coset::RegisteredLabel::Assigned(_) => {
67-
anyhow::bail!(
68-
"Unsupported Content Type: {value:?}, Supported only: {:?}",
69-
ContentType::VARIANTS
70-
.iter()
71-
.map(ToString::to_string)
72-
.collect::<Vec<_>>()
73-
)
74-
},
75-
}
76-
}
77-
}
78-
7957
impl minicbor::Encode<()> for ContentType {
8058
fn encode<W: minicbor::encode::Write>(
8159
&self, e: &mut minicbor::Encoder<W>, _ctx: &mut (),
@@ -86,6 +64,29 @@ impl minicbor::Encode<()> for ContentType {
8664
}
8765
}
8866

67+
impl minicbor::Decode<'_, ()> for ContentType {
68+
fn decode(
69+
d: &mut minicbor::Decoder<'_>, _ctx: &mut (),
70+
) -> Result<Self, minicbor::decode::Error> {
71+
let p = d.position();
72+
match d.int() {
73+
// CoAP Content Format JSON
74+
Ok(val) if val == minicbor::data::Int::from(50_u8) => Ok(Self::Json),
75+
// CoAP Content Format CBOR
76+
Ok(val) if val == minicbor::data::Int::from(60_u8) => Ok(Self::Cbor),
77+
Ok(val) => {
78+
Err(minicbor::decode::Error::message(format!(
79+
"unsupported CoAP Content Formats value: {val}"
80+
)))
81+
},
82+
Err(_) => {
83+
d.set_position(p);
84+
d.str()?.parse().map_err(minicbor::decode::Error::message)
85+
},
86+
}
87+
}
88+
}
89+
8990
#[cfg(test)]
9091
mod tests {
9192
use super::*;

0 commit comments

Comments
 (0)