Skip to content

feat(rust/signed-doc): CBOR encoder using minicbor #351

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

Closed
wants to merge 18 commits into from
Closed
Show file tree
Hide file tree
Changes from 8 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
1 change: 1 addition & 0 deletions .config/dictionaries/project.dic
Original file line number Diff line number Diff line change
Expand Up @@ -326,3 +326,4 @@ xprivate
XPRV
xpub
yoroi
bytewise
49 changes: 49 additions & 0 deletions rust/cbork-utils/src/decode_helper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,26 @@ 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<T, decode::Error>
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::<T>()
)))
}
}

/// Helper function for decoding bytes.
///
/// # Errors
Expand Down Expand Up @@ -75,6 +95,9 @@ pub fn decode_tag(d: &mut Decoder, from: &str) -> Result<Tag, decode::Error> {
}

/// 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
///
Expand All @@ -92,6 +115,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;
Expand Down Expand Up @@ -177,4 +218,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());
}
}
5 changes: 3 additions & 2 deletions rust/signed_doc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = { version = "0.25.1", features = ["std"] }
brotli = "7.0.0"
ed25519-dalek = { version = "2.1.1", features = ["rand_core", "pem"] }
hex = "0.4.3"
Expand Down
118 changes: 55 additions & 63 deletions rust/signed_doc/src/builder.rs
Original file line number Diff line number Diff line change
@@ -1,53 +1,58 @@
//! Catalyst Signed Document Builder.
use catalyst_types::{catalyst_id::CatalystId, problem_report::ProblemReport};

use crate::{
signature::Signature, CatalystSignedDocument, Content, InnerCatalystSignedDocument, Metadata,
Signatures, PROBLEM_REPORT_CTX,
/// An implementation of [`CborMap`].
mod cbor_map;
/// COSE format utils.
mod cose;

use std::convert::Infallible;

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,
};

pub type EncodeError = minicbor::encode::Error<Infallible>;

/// Catalyst Signed Document Builder.
#[derive(Debug)]
pub struct Builder(InnerCatalystSignedDocument);

impl Default for Builder {
fn default() -> Self {
Self::new()
}
pub struct Builder {
/// Mapping from encoded keys to encoded values.
metadata: CborMap,
/// Encoded document content.
content: Vec<u8>,
/// Encoded COSE Signatures.
signatures: Vec<Vec<u8>>,
}

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 {
let report = ProblemReport::new(PROBLEM_REPORT_CTX);
Self(InnerCatalystSignedDocument {
report,
metadata: Metadata::default(),
content: Content::default(),
signatures: Signatures::default(),
raw_bytes: None,
})
pub fn from_content(content: Vec<u8>) -> Self {
Self {
metadata: CborMap::default(),
content: content.into(),
signatures: vec![],
}
}

/// 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<Self> {
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<C, K: minicbor::Encode<C>, V: minicbor::Encode<C>>(
mut self, ctx: &mut C, key: K, v: V,
) -> Result<Self, EncodeError> {
// 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
#[must_use]
pub fn with_decoded_content(mut self, content: Vec<u8>) -> Self {
self.0.content = Content::from_decoded(content);
self
}

/// Add a signature to the document
///
/// # Errors
Expand All @@ -56,43 +61,30 @@ 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,
) -> anyhow::Result<Self> {
let cose_sign = self
.0
.as_cose_sign()
.map_err(|e| anyhow::anyhow!("Failed to sign: {e}"))?;
mut self, sign_fn: impl FnOnce(Vec<u8>) -> Vec<u8>, kid: CatalystId,
) -> Result<Self, EncodeError> {
// Question: maybe this should be cached (e.g. frozen once filled)?
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);
}
let tbs_data = make_tbs_data(&metadata_header, &signature_header, &self.content)?;
let signature_bytes = sign_fn(tbs_data);

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()
}
}

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(),
raw_bytes: None,
})
pub fn build<W: minicbor::encode::Write>(
&self, e: &mut minicbor::Encoder<W>,
) -> Result<(), minicbor::encode::Error<W::Error>> {
let metadata_header = make_metadata_header(&self.metadata);
encode_cose_sign(e, &metadata_header, &self.content, &self.signatures)
}
}
49 changes: 49 additions & 0 deletions rust/signed_doc/src/builder/cbor_map.rs
Original file line number Diff line number Diff line change
@@ -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<u8>, Vec<u8>>);

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<C, K: minicbor::Encode<C>, V: minicbor::Encode<C>>(
&mut self, ctx: &mut C, key: K, v: V,
) -> Result<Option<Vec<u8>>, 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<Item = (&[u8], &[u8])> {
self.0
.iter()
.map(|(key_vec, value_vec)| (key_vec.as_slice(), value_vec.as_slice()))
}
}
Loading