Skip to content

Commit 7f2d501

Browse files
authored
chore(rust/signed-doc): Cleanup Catalyst Signed Document Builder, make it type safe, add special test builder (2). (#380)
* wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * fix * fix * fix clippy
1 parent 6a1be0b commit 7f2d501

File tree

21 files changed

+1299
-1285
lines changed

21 files changed

+1299
-1285
lines changed

rust/signed_doc/bins/mk_signed_doc.rs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,13 +63,11 @@ impl Cli {
6363
println!("{metadata}");
6464
// Load Document from JSON file
6565
let json_doc: serde_json::Value = load_json_from_file(&doc)?;
66-
// Possibly encode if Metadata has an encoding set.
67-
let payload = serde_json::to_vec(&json_doc)?;
6866
// Start with no signatures.
6967
let signed_doc = Builder::new()
7068
.with_json_metadata(metadata)?
71-
.with_decoded_content(payload)?
72-
.build();
69+
.with_json_content(&json_doc)?
70+
.build()?;
7371
println!(
7472
"report {}",
7573
serde_json::to_string(&signed_doc.problem_report())?
@@ -87,7 +85,7 @@ impl Cli {
8785
|message| sk.sign::<()>(&message).to_bytes().to_vec(),
8886
kid.clone(),
8987
)?
90-
.build();
88+
.build()?;
9189
save_signed_doc(new_signed_doc, &doc)?;
9290
},
9391
Self::Inspect { path } => {

rust/signed_doc/src/builder.rs

Lines changed: 136 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,29 @@
11
//! Catalyst Signed Document Builder.
2-
use catalyst_types::{catalyst_id::CatalystId, problem_report::ProblemReport};
2+
use catalyst_types::catalyst_id::CatalystId;
33

44
use crate::{
55
signature::{tbs_data, Signature},
6-
CatalystSignedDocument, Content, Metadata, Signatures,
6+
CatalystSignedDocument, Content, ContentType, Metadata, Signatures,
77
};
88

99
/// Catalyst Signed Document Builder.
10-
#[derive(Debug)]
11-
pub struct Builder {
10+
/// Its a type sage state machine which iterates type safely during different stages of
11+
/// the Catalyst Signed Document build process:
12+
/// Setting Metadata -> Setting Content -> Setting Signatures
13+
pub type Builder = MetadataBuilder;
14+
15+
/// Only `metadata` builder part
16+
pub struct MetadataBuilder(BuilderInner);
17+
18+
/// Only `content` builder part
19+
pub struct ContentBuilder(BuilderInner);
20+
21+
/// Only `Signatures` builder part
22+
pub struct SignaturesBuilder(BuilderInner);
23+
24+
/// Inner state of the Catalyst Signed Documents `Builder`
25+
#[derive(Default)]
26+
pub struct BuilderInner {
1227
/// metadata
1328
metadata: Metadata,
1429
/// content
@@ -17,46 +32,57 @@ pub struct Builder {
1732
signatures: Signatures,
1833
}
1934

20-
impl Default for Builder {
21-
fn default() -> Self {
22-
Self::new()
23-
}
24-
}
25-
26-
impl Builder {
35+
impl MetadataBuilder {
2736
/// Start building a signed document
2837
#[must_use]
38+
#[allow(clippy::new_without_default)]
2939
pub fn new() -> Self {
30-
Self {
31-
metadata: Metadata::default(),
32-
content: Content::default(),
33-
signatures: Signatures::default(),
34-
}
40+
Self(BuilderInner::default())
3541
}
3642

3743
/// Set document metadata in JSON format
3844
/// Collect problem report if some fields are missing.
3945
///
4046
/// # Errors
4147
/// - Fails if it is invalid metadata fields JSON object.
42-
pub fn with_json_metadata(mut self, json: serde_json::Value) -> anyhow::Result<Self> {
43-
self.metadata = Metadata::from_json(json, &ProblemReport::new(""));
44-
Ok(self)
48+
pub fn with_json_metadata(mut self, json: serde_json::Value) -> anyhow::Result<ContentBuilder> {
49+
self.0.metadata = Metadata::from_json(json)?;
50+
Ok(ContentBuilder(self.0))
51+
}
52+
}
53+
54+
impl ContentBuilder {
55+
/// Sets an empty content
56+
pub fn empty_content(self) -> SignaturesBuilder {
57+
SignaturesBuilder(self.0)
4558
}
4659

47-
/// Set decoded (original) document content bytes
60+
/// Set the provided JSON content, applying already set `content-encoding`.
4861
///
4962
/// # Errors
63+
/// - Verifies that `content-type` field is set to JSON
64+
/// - Cannot serialize provided JSON
5065
/// - Compression failure
51-
pub fn with_decoded_content(mut self, decoded: Vec<u8>) -> anyhow::Result<Self> {
52-
if let Some(encoding) = self.metadata.content_encoding() {
53-
self.content = encoding.encode(&decoded)?.into();
66+
pub fn with_json_content(
67+
mut self, json: &serde_json::Value,
68+
) -> anyhow::Result<SignaturesBuilder> {
69+
anyhow::ensure!(
70+
self.0.metadata.content_type()? == ContentType::Json,
71+
"Already set metadata field `content-type` is not JSON value"
72+
);
73+
74+
let content = serde_json::to_vec(&json)?;
75+
if let Some(encoding) = self.0.metadata.content_encoding() {
76+
self.0.content = encoding.encode(&content)?.into();
5477
} else {
55-
self.content = decoded.into();
78+
self.0.content = content.into();
5679
}
57-
Ok(self)
80+
81+
Ok(SignaturesBuilder(self.0))
5882
}
83+
}
5984

85+
impl SignaturesBuilder {
6086
/// Add a signature to the document
6187
///
6288
/// # Errors
@@ -70,49 +96,104 @@ impl Builder {
7096
if kid.is_id() {
7197
anyhow::bail!("Provided kid should be in a uri format, kid: {kid}");
7298
}
73-
let data_to_sign = tbs_data(&kid, &self.metadata, &self.content)?;
99+
let data_to_sign = tbs_data(&kid, &self.0.metadata, &self.0.content)?;
74100
let sign_bytes = sign_fn(data_to_sign);
75-
self.signatures.push(Signature::new(kid, sign_bytes));
101+
self.0.signatures.push(Signature::new(kid, sign_bytes));
76102

77103
Ok(self)
78104
}
79105

80-
/// Build a signed document with the collected error report.
81-
/// Could provide an invalid document.
106+
/// Builds a document from the set `metadata`, `content` and `signatures`.
82107
///
83-
/// # Panics
84-
/// Should not panic
85-
#[must_use]
86-
#[allow(
87-
clippy::unwrap_used,
88-
reason = "At this point all the data MUST be correctly encodable, and the final prepared bytes MUST be correctly decodable as a CatalystSignedDocument object."
89-
)]
90-
pub fn build(self) -> CatalystSignedDocument {
91-
let mut e = minicbor::Encoder::new(Vec::new());
92-
// COSE_Sign tag
93-
// <!https://datatracker.ietf.org/doc/html/rfc8152#page-9>
94-
e.tag(minicbor::data::Tag::new(98)).unwrap();
95-
e.array(4).unwrap();
96-
// protected headers (metadata fields)
97-
e.bytes(minicbor::to_vec(&self.metadata).unwrap().as_slice())
98-
.unwrap();
99-
// empty unprotected headers
100-
e.map(0).unwrap();
101-
// content
102-
e.encode(&self.content).unwrap();
103-
// signatures
104-
e.encode(self.signatures).unwrap();
105-
106-
CatalystSignedDocument::try_from(e.into_writer().as_slice()).unwrap()
108+
/// # Errors:
109+
/// - CBOR encoding/decoding failures
110+
pub fn build(self) -> anyhow::Result<CatalystSignedDocument> {
111+
let doc = build_document(&self.0.metadata, &self.0.content, &self.0.signatures)?;
112+
Ok(doc)
107113
}
108114
}
109115

110-
impl From<&CatalystSignedDocument> for Builder {
116+
/// Build document from the provided `metadata`, `content` and `signatures`, performs all
117+
/// the decoding validation and collects a problem report.
118+
fn build_document(
119+
metadata: &Metadata, content: &Content, signatures: &Signatures,
120+
) -> anyhow::Result<CatalystSignedDocument> {
121+
let mut e = minicbor::Encoder::new(Vec::new());
122+
// COSE_Sign tag
123+
// <!https://datatracker.ietf.org/doc/html/rfc8152#page-9>
124+
e.tag(minicbor::data::Tag::new(98))?;
125+
e.array(4)?;
126+
// protected headers (metadata fields)
127+
e.bytes(minicbor::to_vec(metadata)?.as_slice())?;
128+
// empty unprotected headers
129+
e.map(0)?;
130+
// content
131+
e.encode(content)?;
132+
// signatures
133+
e.encode(signatures)?;
134+
CatalystSignedDocument::try_from(e.into_writer().as_slice())
135+
}
136+
137+
impl From<&CatalystSignedDocument> for SignaturesBuilder {
111138
fn from(value: &CatalystSignedDocument) -> Self {
112-
Self {
139+
Self(BuilderInner {
113140
metadata: value.inner.metadata.clone(),
114141
content: value.inner.content.clone(),
115142
signatures: value.inner.signatures.clone(),
143+
})
144+
}
145+
}
146+
147+
#[cfg(test)]
148+
pub(crate) mod tests {
149+
use crate::builder::SignaturesBuilder;
150+
151+
/// A test version of the builder, which allows to build a not fully valid catalyst
152+
/// signed document
153+
pub(crate) struct Builder(super::BuilderInner);
154+
155+
impl Default for Builder {
156+
fn default() -> Self {
157+
Self::new()
158+
}
159+
}
160+
161+
impl Builder {
162+
/// Start building a signed document
163+
#[must_use]
164+
pub(crate) fn new() -> Self {
165+
Self(super::BuilderInner::default())
166+
}
167+
168+
/// Add provided `SupportedField` into the `Metadata`.
169+
pub(crate) fn with_metadata_field(
170+
mut self, field: crate::metadata::SupportedField,
171+
) -> Self {
172+
self.0.metadata.add_field(field);
173+
self
174+
}
175+
176+
/// Set the content (COSE payload) to the document builder.
177+
/// It will set the content as its provided, make sure by yourself that
178+
/// `content-type` and `content-encoding` fields are aligned with the
179+
/// provided content bytes.
180+
pub(crate) fn with_content(mut self, content: Vec<u8>) -> Self {
181+
self.0.content = content.into();
182+
self
183+
}
184+
185+
/// Add a signature to the document
186+
pub(crate) fn add_signature(
187+
mut self, sign_fn: impl FnOnce(Vec<u8>) -> Vec<u8>, kid: super::CatalystId,
188+
) -> anyhow::Result<Self> {
189+
self.0 = SignaturesBuilder(self.0).add_signature(sign_fn, kid)?.0;
190+
Ok(self)
191+
}
192+
193+
/// Build a signed document with the collected error report.
194+
/// Could provide an invalid document.
195+
pub(crate) fn build(self) -> super::CatalystSignedDocument {
196+
super::build_document(&self.0.metadata, &self.0.content, &self.0.signatures).unwrap()
116197
}
117198
}
118199
}

rust/signed_doc/src/lib.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ pub use metadata::{
2929
use minicbor::{decode, encode, Decode, Decoder, Encode};
3030
pub use signature::{CatalystId, Signatures};
3131

32+
use crate::builder::SignaturesBuilder;
33+
3234
/// A problem report content string
3335
const PROBLEM_REPORT_CTX: &str = "Catalyst Signed Document";
3436

@@ -203,7 +205,7 @@ impl CatalystSignedDocument {
203205
/// Returns a signed document `Builder` pre-loaded with the current signed document's
204206
/// data.
205207
#[must_use]
206-
pub fn into_builder(&self) -> Builder {
208+
pub fn into_builder(&self) -> SignaturesBuilder {
207209
self.into()
208210
}
209211
}

rust/signed_doc/src/metadata/mod.rs

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,8 @@ pub use section::Section;
2323
use strum::IntoDiscriminant as _;
2424
use utils::{cose_protected_header_find, decode_document_field_from_protected_header, CborUuidV7};
2525

26-
use crate::{
27-
decode_context::DecodeContext,
28-
metadata::{
29-
supported_field::{SupportedField, SupportedLabel},
30-
utils::decode_cose_protected_header_value,
31-
},
32-
};
26+
pub(crate) use crate::metadata::supported_field::{SupportedField, SupportedLabel};
27+
use crate::{decode_context::DecodeContext, metadata::utils::decode_cose_protected_header_value};
3328

3429
/// `content_encoding` field COSE key value
3530
const CONTENT_ENCODING_KEY: &str = "Content-Encoding";
@@ -171,6 +166,18 @@ impl Metadata {
171166
.and_then(SupportedField::try_as_parameters_ref)
172167
}
173168

169+
/// Add `SupportedField` into the `Metadata`.
170+
///
171+
/// # Warning
172+
///
173+
/// Building metadata by-field with this function doesn't ensure the presence of
174+
/// required fields. Use [`Self::from_fields`] or [`Self::from_json`] if it's
175+
/// important for metadata to be valid.
176+
#[cfg(test)]
177+
pub(crate) fn add_field(&mut self, field: SupportedField) {
178+
self.0.insert(field.discriminant(), field);
179+
}
180+
174181
/// Build `Metadata` object from the metadata fields, doing all necessary validation.
175182
pub(crate) fn from_fields(fields: Vec<SupportedField>, report: &ProblemReport) -> Self {
176183
const REPORT_CONTEXT: &str = "Metadata building";
@@ -204,16 +211,12 @@ impl Metadata {
204211
}
205212

206213
/// Build `Metadata` object from the metadata fields, doing all necessary validation.
207-
pub(crate) fn from_json(fields: serde_json::Value, report: &ProblemReport) -> Self {
208-
let fields = serde::Deserializer::deserialize_map(fields, MetadataDeserializeVisitor)
209-
.inspect_err(|err| {
210-
report.other(
211-
&format!("Unable to deserialize json: {err}"),
212-
"Metadata building from json",
213-
);
214-
})
215-
.unwrap_or_default();
216-
Self::from_fields(fields, report)
214+
pub(crate) fn from_json(fields: serde_json::Value) -> anyhow::Result<Self> {
215+
let fields = serde::Deserializer::deserialize_map(fields, MetadataDeserializeVisitor)?;
216+
let report = ProblemReport::new("Deserializing metadata from json");
217+
let metadata = Self::from_fields(fields, &report);
218+
anyhow::ensure!(!report.is_problematic(), "{:?}", report);
219+
Ok(metadata)
217220
}
218221
}
219222

rust/signed_doc/src/metadata/supported_field.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ impl Display for Label<'_> {
9595
)]
9696
#[non_exhaustive]
9797
#[repr(usize)]
98-
pub enum SupportedField {
98+
pub(crate) enum SupportedField {
9999
/// `content-type` field. In COSE it's represented as the signed integer `3` (see [RFC
100100
/// 8949 section 3.1]).
101101
///

0 commit comments

Comments
 (0)