Skip to content

feat(rust/signed-doc): Catalyst signed document encoding using minicbor #353

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 24 commits into from
Jun 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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: 9 additions & 0 deletions rust/catalyst-types/src/catalyst_id/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -676,6 +676,15 @@ 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(())
}
}

#[cfg(test)]
mod tests {
use chrono::{DateTime, Utc};
Expand Down
5 changes: 4 additions & 1 deletion rust/signed_doc/bins/mk_signed_doc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,10 @@ impl Cli {

let new_signed_doc = signed_doc
.into_builder()
.add_signature(|message| sk.sign::<()>(&message).to_bytes().to_vec(), &kid)?
.add_signature(
|message| sk.sign::<()>(&message).to_bytes().to_vec(),
kid.clone(),
)?
.build();
save_signed_doc(new_signed_doc, &doc)?;
},
Expand Down
26 changes: 9 additions & 17 deletions rust/signed_doc/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
use catalyst_types::{catalyst_id::CatalystId, problem_report::ProblemReport};

use crate::{
signature::Signature, CatalystSignedDocument, Content, InnerCatalystSignedDocument, Metadata,
Signatures, PROBLEM_REPORT_CTX,
signature::{tbs_data, Signature},
CatalystSignedDocument, Content, InnerCatalystSignedDocument, Metadata, Signatures,
PROBLEM_REPORT_CTX,
};

/// Catalyst Signed Document Builder.
Expand Down Expand Up @@ -56,23 +57,14 @@ 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<u8>) -> Vec<u8>, kid: &CatalystId,
mut self, sign_fn: impl FnOnce(Vec<u8>) -> Vec<u8>, kid: CatalystId,
) -> anyhow::Result<Self> {
let cose_sign = self
.0
.as_cose_sign()
.map_err(|e| anyhow::anyhow!("Failed to sign: {e}"))?;

let protected_header = coset::HeaderBuilder::new().key_id(kid.to_string().into_bytes());

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);
if kid.is_id() {
anyhow::bail!("Provided kid should be in a uri format, kid: {kid}");
}
let data_to_sign = tbs_data(&kid, &self.0.metadata, &self.0.content)?;
let sign_bytes = sign_fn(data_to_sign);
self.0.signatures.push(Signature::new(kid, sign_bytes));

Ok(self)
}
Expand Down
98 changes: 42 additions & 56 deletions rust/signed_doc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,13 @@ use std::{
sync::Arc,
};

use anyhow::Context;
pub use builder::Builder;
pub use catalyst_types::{
problem_report::ProblemReport,
uuid::{Uuid, UuidV4, UuidV7},
};
pub use content::Content;
use coset::{CborSerializable, Header, TaggedCborSerializable};
use coset::{CborSerializable, TaggedCborSerializable};
use decode_context::{CompatibilityPolicy, DecodeContext};
pub use metadata::{ContentEncoding, ContentType, DocType, DocumentRef, Metadata, Section};
use minicbor::{decode, encode, Decode, Decoder, Encode};
Expand Down Expand Up @@ -68,7 +67,7 @@ impl Display for CatalystSignedDocument {
if self.inner.signatures.is_empty() {
writeln!(f, " This document is unsigned.")?;
} else {
for kid in &self.inner.signatures.kids() {
for kid in &self.kids() {
writeln!(f, " Signature Key ID: {kid}")?;
}
}
Expand Down Expand Up @@ -147,13 +146,21 @@ impl CatalystSignedDocument {
/// Return a list of Document's Catalyst IDs.
#[must_use]
pub fn kids(&self) -> Vec<CatalystId> {
self.inner.signatures.kids()
self.inner
.signatures
.iter()
.map(|s| s.kid().clone())
.collect()
}

/// Return a list of Document's author IDs (short form of Catalyst IDs).
#[must_use]
pub fn authors(&self) -> Vec<CatalystId> {
self.inner.signatures.authors()
self.inner
.signatures
.iter()
.map(|s| s.kid().as_short_id())
.collect()
}

/// Returns a collected problem report for the document.
Expand All @@ -173,14 +180,6 @@ impl CatalystSignedDocument {
&self.inner.report
}

/// Convert Catalyst Signed Document into `coset::CoseSign`
///
/// # Errors
/// Could fails if the `CatalystSignedDocument` object is not valid.
pub(crate) fn as_cose_sign(&self) -> anyhow::Result<coset::CoseSign> {
self.inner.as_cose_sign()
}

/// Returns a signed document `Builder` pre-loaded with the current signed document's
/// data.
#[must_use]
Expand All @@ -189,39 +188,6 @@ impl CatalystSignedDocument {
}
}

impl InnerCatalystSignedDocument {
/// Convert Catalyst Signed Document into `coset::CoseSign`
///
/// # Errors
/// Could fails if the `CatalystSignedDocument` object is not valid.
fn as_cose_sign(&self) -> anyhow::Result<coset::CoseSign> {
if let Some(raw_bytes) = self.raw_bytes.clone() {
let cose_sign = coset::CoseSign::from_tagged_slice(raw_bytes.as_slice())
.or_else(|_| coset::CoseSign::from_slice(raw_bytes.as_slice()))
.map_err(|e| {
minicbor::decode::Error::message(format!("Invalid COSE Sign document: {e}"))
})?;
Ok(cose_sign)
} else {
let protected_header =
Header::try_from(&self.metadata).context("Failed to encode Document Metadata")?;

let content = self
.content
.encoded_bytes(self.metadata.content_encoding())?;

let mut builder = coset::CoseSignBuilder::new()
.protected(protected_header)
.payload(content);

for signature in self.signatures.cose_signatures() {
builder = builder.add_signature(signature);
}
Ok(builder.build())
}
}
}

impl Decode<'_, ()> for CatalystSignedDocument {
fn decode(d: &mut Decoder<'_>, _ctx: &mut ()) -> Result<Self, decode::Error> {
let start = d.position();
Expand Down Expand Up @@ -264,18 +230,38 @@ impl Decode<'_, ()> for CatalystSignedDocument {
}
}

impl Encode<()> for CatalystSignedDocument {
impl<C> Encode<C> for CatalystSignedDocument {
fn encode<W: minicbor::encode::Write>(
&self, e: &mut encode::Encoder<W>, _ctx: &mut (),
&self, e: &mut encode::Encoder<W>, _ctx: &mut C,
) -> Result<(), encode::Error<W::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)
.map_err(|_| minicbor::encode::Error::message("Failed to encode to CBOR"))
if let Some(raw_bytes) = &self.inner.raw_bytes {
e.writer_mut()
.write_all(raw_bytes)
.map_err(minicbor::encode::Error::write)?;
} else {
// COSE_Sign tag
// <!https://datatracker.ietf.org/doc/html/rfc8152#page-9>
e.tag(minicbor::data::Tag::new(98))?;
e.array(4)?;
// protected headers (metadata fields)
e.bytes(
minicbor::to_vec(self.doc_meta())
.map_err(minicbor::encode::Error::message)?
.as_slice(),
)?;
// empty unprotected headers
e.map(0)?;
// content
let content = self
.doc_content()
.encoded_bytes(self.doc_content_encoding())
.map_err(minicbor::encode::Error::message)?;
e.bytes(content.as_slice())?;
// signatures
e.encode(self.signatures())?;
}

Ok(())
}
}

Expand Down
9 changes: 9 additions & 0 deletions rust/signed_doc/src/metadata/content_encoding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,12 @@ impl TryFrom<&coset::cbor::Value> for ContentEncoding {
}
}
}

impl minicbor::Encode<()> for ContentEncoding {
fn encode<W: minicbor::encode::Write>(
&self, e: &mut minicbor::Encoder<W>, _ctx: &mut (),
) -> Result<(), minicbor::encode::Error<W::Error>> {
e.str(self.to_string().as_str())?;
Ok(())
}
}
33 changes: 17 additions & 16 deletions rust/signed_doc/src/metadata/content_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,33 +55,34 @@ impl<'de> Deserialize<'de> for ContentType {
}
}

impl From<ContentType> for CoapContentFormat {
fn from(value: ContentType) -> Self {
match value {
ContentType::Cbor => Self::Cbor,
ContentType::Json => Self::Json,
}
}
}

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

fn try_from(value: &coset::ContentType) -> Result<Self, Self::Error> {
let content_type = match value {
coset::ContentType::Assigned(CoapContentFormat::Json) => ContentType::Json,
coset::ContentType::Assigned(CoapContentFormat::Cbor) => ContentType::Cbor,
_ => {
match value {
coset::ContentType::Assigned(CoapContentFormat::Json) => Ok(ContentType::Json),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The aim is to remove coset, is it not?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inside this PR, I've just removing coset for encoding functionality. Decoding would be done under the different PR

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: {:?}",
"Unsupported Content Type: {value:?}, Supported only: {:?}",
ContentType::VARIANTS
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
)
},
};
Ok(content_type)
}
}
}

impl minicbor::Encode<()> for ContentType {
fn encode<W: minicbor::encode::Write>(
&self, e: &mut minicbor::Encoder<W>, _ctx: &mut (),
) -> Result<(), minicbor::encode::Error<W::Error>> {
// encode as media types, not in CoAP Content-Formats
e.str(self.to_string().as_str())?;
Ok(())
}
}

Expand Down
Loading