Skip to content
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 3 additions & 6 deletions rust/catalyst-types/src/catalyst_id/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -676,12 +676,9 @@ impl TryFrom<&[u8]> for CatalystId {
}
}

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

Expand Down
29 changes: 19 additions & 10 deletions rust/cbork-utils/src/deterministic_helper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,12 +98,11 @@ impl Ord for MapEntry {
/// - Map keys are not properly sorted (`UnorderedMapKeys`)
/// - Duplicate keys are found (`DuplicateMapKey`)
/// - Map key or value decoding fails (`DecoderError`)
pub fn decode_map_deterministically(d: &mut Decoder) -> Result<Vec<u8>, minicbor::decode::Error> {
pub fn decode_map_deterministically(
d: &mut Decoder,
) -> Result<Vec<MapEntry>, minicbor::decode::Error> {
validate_input_not_empty(d)?;

// Store the starting position BEFORE consuming the map header
let map_start = d.position();

// From RFC 8949 Section 4.2.2:
// "Indefinite-length items must be made definite-length items."
// The specification explicitly prohibits indefinite-length items in
Expand All @@ -115,18 +114,14 @@ pub fn decode_map_deterministically(d: &mut Decoder) -> Result<Vec<u8>, minicbor
})?;

let header_end_pos = d.position();

check_map_minimal_length(d, header_end_pos, map_len)?;

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

validate_map_ordering(&entries)?;

// Get the ending position after validation
let map_end = d.position();

get_bytes(d, map_start, map_end)
Ok(entries)
}

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

// Verify we got back exactly the same bytes
assert_eq!(result, valid_map);

assert_eq!(result, vec![
MapEntry {
// Key 1: 2-byte string
key_bytes: vec![0x42, 0x01, 0x02],
// Value 1: 1-byte string
value: vec![0x41, 0x01]
},
MapEntry {
// Key 2: 3-byte string
key_bytes: vec![0x43, 0x01, 0x02, 0x03,],
// Value 2: 1-byte string
value: vec![0x41, 0x02,]
}
]);
}

/// Test cases for lexicographic ordering of map keys as specified in RFC 8949 Section
Expand Down
2 changes: 1 addition & 1 deletion rust/signed_doc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ workspace = true

[dependencies]
catalyst-types = { version = "0.0.3", path = "../catalyst-types" }
cbork-utils = { version = "0.0.1", path = "../cbork-utils" }

anyhow = "1.0.95"
serde = { version = "1.0.217", features = ["derive"] }
serde_json = { version = "1.0.134", features = ["raw_value"] }
coset = "0.3.8"
minicbor = { version = "0.25.1", features = ["half"] }
brotli = "7.0.0"
ed25519-dalek = { version = "2.1.1", features = ["rand_core", "pem"] }
Expand Down
15 changes: 15 additions & 0 deletions rust/signed_doc/src/content.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,18 @@ impl minicbor::Encode<()> for Content {
Ok(())
}
}

impl minicbor::Decode<'_, ()> for Content {
fn decode(
d: &mut minicbor::Decoder<'_>, _ctx: &mut (),
) -> Result<Self, minicbor::decode::Error> {
let p = d.position();
d.null()
.map(|()| Self(Vec::new()))
// important to use `or_else` so it will lazy evaluated at the time when it is needed
.or_else(|_| {
d.set_position(p);
d.bytes().map(Vec::from).map(Self)
})
}
}
64 changes: 39 additions & 25 deletions rust/signed_doc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ pub use catalyst_types::{
uuid::{Uuid, UuidV4, UuidV7},
};
pub use content::Content;
use coset::{CborSerializable, TaggedCborSerializable};
use decode_context::{CompatibilityPolicy, DecodeContext};
pub use metadata::{
ContentEncoding, ContentType, DocLocator, DocType, DocumentRef, DocumentRefs, Metadata, Section,
Expand All @@ -31,8 +30,8 @@ pub use signature::{CatalystId, Signatures};

use crate::builder::SignaturesBuilder;

/// A problem report content string
const PROBLEM_REPORT_CTX: &str = "Catalyst Signed Document";
/// <https://datatracker.ietf.org/doc/html/rfc8152#page-8>
const COSE_SIGN_CBOR_TAG: minicbor::data::Tag = minicbor::data::Tag::new(98);

/// Inner type that holds the Catalyst Signed Document with parsing errors.
#[derive(Debug)]
Expand Down Expand Up @@ -212,34 +211,49 @@ impl CatalystSignedDocument {

impl Decode<'_, ()> for CatalystSignedDocument {
fn decode(d: &mut Decoder<'_>, _ctx: &mut ()) -> Result<Self, decode::Error> {
let start = d.position();
d.skip()?;
let end = d.position();
let cose_bytes = d
.input()
.get(start..end)
.ok_or(minicbor::decode::Error::end_of_input())?;

let cose_sign = coset::CoseSign::from_tagged_slice(cose_bytes)
.or_else(|_| coset::CoseSign::from_slice(cose_bytes))
.map_err(|e| {
minicbor::decode::Error::message(format!("Invalid COSE Sign document: {e}"))
})?;

let mut report = ProblemReport::new(PROBLEM_REPORT_CTX);
let mut report = ProblemReport::new("Catalyst Signed Document Decoding");
let mut ctx = DecodeContext {
compatibility_policy: CompatibilityPolicy::Accept,
report: &mut report,
};
let metadata = Metadata::from_protected_header(&cose_sign.protected, &mut ctx);
let signatures = Signatures::from_cose_sig_list(&cose_sign.signatures, &report);
let start = d.position();

let content = if let Some(payload) = cose_sign.payload {
payload.into()
if let Ok(tag) = d.tag() {
if tag != COSE_SIGN_CBOR_TAG {
return Err(minicbor::decode::Error::message(format!(
"Must be equal to the COSE_Sign tag value: {COSE_SIGN_CBOR_TAG}"
)));
}
} else {
report.missing_field("COSE Sign Payload", "Missing document content (payload)");
Content::default()
};
d.set_position(start);
}

if !matches!(d.array()?, Some(4)) {
return Err(minicbor::decode::Error::message(
"Must be a definite size array of 4 elements",
));
}

let metadata_bytes = d.bytes()?;
let metadata = Metadata::decode(&mut minicbor::Decoder::new(metadata_bytes), &mut ctx)?;

// empty unprotected headers
let mut map =
cbork_utils::deterministic_helper::decode_map_deterministically(d)?.into_iter();
if map.next().is_some() {
return Err(minicbor::decode::Error::message(
"COSE unprotected headers must be empty",
));
}

let content = Content::decode(d, &mut ())?;
let signatures = Signatures::decode(d, &mut ctx)?;

let end = d.position();
let cose_bytes = d
.input()
.get(start..end)
.ok_or(minicbor::decode::Error::end_of_input())?;

Ok(InnerCatalystSignedDocument {
metadata,
Expand Down
21 changes: 8 additions & 13 deletions rust/signed_doc/src/metadata/content_encoding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,19 +72,6 @@ impl<'de> Deserialize<'de> for ContentEncoding {
}
}

impl TryFrom<&coset::cbor::Value> for ContentEncoding {
type Error = anyhow::Error;

fn try_from(val: &coset::cbor::Value) -> anyhow::Result<ContentEncoding> {
match val.as_text() {
Some(encoding) => encoding.parse(),
None => {
anyhow::bail!("Expected Content Encoding to be a string");
},
}
}
}

impl minicbor::Encode<()> for ContentEncoding {
fn encode<W: minicbor::encode::Write>(
&self, e: &mut minicbor::Encoder<W>, _ctx: &mut (),
Expand All @@ -93,3 +80,11 @@ impl minicbor::Encode<()> for ContentEncoding {
Ok(())
}
}

impl minicbor::Decode<'_, ()> for ContentEncoding {
fn decode(
d: &mut minicbor::Decoder<'_>, _ctx: &mut (),
) -> Result<Self, minicbor::decode::Error> {
d.str()?.parse().map_err(minicbor::decode::Error::message)
}
}
45 changes: 23 additions & 22 deletions rust/signed_doc/src/metadata/content_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ use std::{
str::FromStr,
};

use coset::iana::CoapContentFormat;
use serde::{de, Deserialize, Deserializer};
use strum::VariantArray;

Expand Down Expand Up @@ -55,27 +54,6 @@ impl<'de> Deserialize<'de> for ContentType {
}
}

impl TryFrom<&coset::ContentType> for ContentType {
type Error = anyhow::Error;

fn try_from(value: &coset::ContentType) -> Result<Self, Self::Error> {
match value {
coset::ContentType::Assigned(CoapContentFormat::Json) => Ok(ContentType::Json),
coset::ContentType::Assigned(CoapContentFormat::Cbor) => Ok(ContentType::Cbor),
coset::ContentType::Text(str) => str.parse(),
coset::RegisteredLabel::Assigned(_) => {
anyhow::bail!(
"Unsupported Content Type: {value:?}, Supported only: {:?}",
ContentType::VARIANTS
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
)
},
}
}
}

impl minicbor::Encode<()> for ContentType {
fn encode<W: minicbor::encode::Write>(
&self, e: &mut minicbor::Encoder<W>, _ctx: &mut (),
Expand All @@ -86,6 +64,29 @@ impl minicbor::Encode<()> for ContentType {
}
}

impl minicbor::Decode<'_, ()> for ContentType {
fn decode(
d: &mut minicbor::Decoder<'_>, _ctx: &mut (),
) -> Result<Self, minicbor::decode::Error> {
let p = d.position();
match d.int() {
// CoAP Content Format JSON
Ok(val) if val == minicbor::data::Int::from(50_u8) => Ok(Self::Json),
// CoAP Content Format CBOR
Ok(val) if val == minicbor::data::Int::from(60_u8) => Ok(Self::Cbor),
Ok(val) => {
Err(minicbor::decode::Error::message(format!(
"unsupported CoAP Content Formats value: {val}"
)))
},
Err(_) => {
d.set_position(p);
d.str()?.parse().map_err(minicbor::decode::Error::message)
},
}
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
32 changes: 1 addition & 31 deletions rust/signed_doc/src/metadata/doc_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ use std::{
hash::{Hash, Hasher},
};

use catalyst_types::uuid::{CborContext, Uuid, UuidV4, UUID_CBOR_TAG};
use coset::cbor::Value;
use catalyst_types::uuid::{CborContext, Uuid, UuidV4};
use minicbor::{Decode, Decoder, Encode};
use serde::{Deserialize, Deserializer};
use tracing::warn;
Expand Down Expand Up @@ -296,23 +295,6 @@ impl<'de> Deserialize<'de> for DocType {
}
}

impl From<DocType> for Value {
fn from(value: DocType) -> Self {
Value::Array(
value
.0
.iter()
.map(|uuidv4| {
Value::Tag(
UUID_CBOR_TAG,
Box::new(Value::Bytes(uuidv4.uuid().as_bytes().to_vec())),
)
})
.collect(),
)
}
}

// This is needed to preserve backward compatibility with the old solution.
impl PartialEq for DocType {
fn eq(&self, other: &Self) -> bool {
Expand Down Expand Up @@ -448,18 +430,6 @@ mod tests {
assert!(matches!(result, Err(DocTypeError::StringConversion(s)) if s == "not-a-uuid"));
}

#[test]
fn test_doc_type_to_value() {
let uuid = uuid::Uuid::new_v4();
let doc_type: Value = DocType(vec![UuidV4::try_from(uuid).unwrap()]).into();

for d in &doc_type.into_array().unwrap() {
let t = d.clone().into_tag().unwrap();
assert_eq!(t.0, UUID_CBOR_TAG);
assert_eq!(t.1.as_bytes().unwrap().len(), 16);
}
}

#[test]
fn test_doctype_equal_special_cases() {
// Direct equal
Expand Down
25 changes: 0 additions & 25 deletions rust/signed_doc/src/metadata/document_refs/doc_locator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
use std::fmt::Display;

use catalyst_types::problem_report::ProblemReport;
use coset::cbor::Value;
use minicbor::{Decode, Decoder, Encode};

/// CBOR tag of IPLD content identifiers (CIDs).
Expand Down Expand Up @@ -47,15 +46,6 @@ impl Display for DocLocator {
}
}

impl From<DocLocator> for Value {
fn from(value: DocLocator) -> Self {
Value::Map(vec![(
Value::Text(CID_MAP_KEY.to_string()),
Value::Tag(CID_TAG, Box::new(Value::Bytes(value.0.clone()))),
)])
}
}

// document_locator = { "cid" => cid }
impl Decode<'_, ProblemReport> for DocLocator {
fn decode(
Expand Down Expand Up @@ -157,19 +147,4 @@ mod tests {
let decoded_doc_loc = DocLocator::decode(&mut decoder, &mut report).unwrap();
assert_eq!(locator, decoded_doc_loc);
}

#[test]
#[allow(clippy::indexing_slicing)]
fn test_doc_locator_to_value() {
let data = vec![1, 2, 3, 4];
let locator = DocLocator(data.clone());
let value: Value = locator.into();
let map = value.into_map().unwrap();
assert_eq!(map.len(), usize::try_from(DOC_LOC_MAP_ITEM).unwrap());
let key = map[0].0.clone().into_text().unwrap();
assert_eq!(key, CID_MAP_KEY);
let (tag, value) = map[0].1.clone().into_tag().unwrap();
assert_eq!(tag, CID_TAG);
assert_eq!(value.into_bytes().unwrap(), data);
}
}
Loading