• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

input-output-hk / catalyst-libs / 19740735823

27 Nov 2025 03:12PM UTC coverage: 67.771% (+0.03%) from 67.745%
19740735823

push

github

web-flow
feat(rust/signed-doc): Update the signed doc specification to the 0.2.0 version (#660)

* Update the signed doc specification to the 0.2.0 version

* Use attribute instead of commenting the tests

* Add missing space

* Apply review comments

13 of 17 new or added lines in 1 file covered. (76.47%)

9 existing lines in 3 files now uncovered.

13927 of 20550 relevant lines covered (67.77%)

2912.04 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.59
/rust/signed_doc/src/builder.rs
1
//! Catalyst Signed Document Builder.
2
use std::io::Write;
3

4
use catalyst_types::catalyst_id::CatalystId;
5
use cbork_utils::with_cbor_bytes::WithCborBytes;
6

7
use crate::{
8
    CatalystSignedDocument, Content, ContentType, Metadata, Signatures,
9
    signature::{Signature, tbs_data},
10
};
11

12
/// Catalyst Signed Document Builder.
13
/// Its a type sage state machine which iterates type safely during different stages of
14
/// the Catalyst Signed Document build process:
15
/// Setting Metadata -> Setting Content -> Setting Signatures
16
pub type Builder = MetadataBuilder;
17

18
/// Only `metadata` builder part
19
pub struct MetadataBuilder {
20
    /// metadata
21
    metadata: Metadata,
22
}
23

24
/// Only `content` builder part
25
pub struct ContentBuilder {
26
    /// metadata
27
    metadata: Metadata,
28
    /// content
29
    content: Content,
30
}
31

32
/// Only `Signatures` builder part
33
pub struct SignaturesBuilder {
34
    /// metadata
35
    metadata: WithCborBytes<Metadata>,
36
    /// content
37
    content: Content,
38
    /// signatures
39
    signatures: Signatures,
40
}
41

42
impl MetadataBuilder {
43
    /// Start building a signed document
44
    #[must_use]
45
    #[allow(clippy::new_without_default)]
46
    pub fn new() -> Self {
480✔
47
        Self {
480✔
48
            metadata: Metadata::default(),
480✔
49
        }
480✔
50
    }
480✔
51

52
    /// Set document metadata in JSON format
53
    /// Collect problem report if some fields are missing.
54
    ///
55
    /// # Errors
56
    /// - Fails if it is invalid metadata fields JSON object.
57
    pub fn with_json_metadata(
480✔
58
        mut self,
480✔
59
        json: serde_json::Value,
480✔
60
    ) -> anyhow::Result<ContentBuilder> {
480✔
61
        self.metadata = Metadata::from_json(json)?;
480✔
62
        Ok(ContentBuilder {
480✔
63
            metadata: self.metadata,
480✔
64
            content: Content::default(),
480✔
65
        })
480✔
66
    }
480✔
67
}
68

69
impl ContentBuilder {
70
    /// Prepares a `SignaturesBuilder` from the current `ContentBuilder`
71
    fn into_signatures_builder(self) -> anyhow::Result<SignaturesBuilder> {
480✔
72
        Ok(SignaturesBuilder {
73
            metadata: WithCborBytes::new(self.metadata, &mut ())?,
480✔
74
            content: self.content,
480✔
75
            signatures: Signatures::default(),
480✔
76
        })
77
    }
480✔
78

79
    /// Sets an empty content
80
    pub fn empty_content(self) -> anyhow::Result<SignaturesBuilder> {
15✔
81
        self.into_signatures_builder()
15✔
82
    }
15✔
83

84
    /// Sets the provided CBOR content, applying already set `content-encoding`.
85
    ///
86
    /// # Errors
87
    ///  - Verifies that `content-type` field is set to CBOR.
88
    ///  - Cannot serialize provided JSON.
89
    ///  - Compression failure.
90
    pub fn with_cbor_content<T: minicbor::Encode<()>>(
19✔
91
        mut self,
19✔
92
        content: T,
19✔
93
    ) -> anyhow::Result<SignaturesBuilder> {
19✔
94
        anyhow::ensure!(
19✔
95
            self.metadata.content_type() == Some(ContentType::Cbor),
19✔
NEW
96
            "Already set metadata field `content-type` is not CBOR value"
×
97
        );
98

99
        let mut buffer = Vec::new();
19✔
100
        let mut encoder = minicbor::Encoder::new(&mut buffer);
19✔
101
        content.encode(&mut encoder, &mut ())?;
19✔
102

103
        if let Some(encoding) = self.metadata.content_encoding() {
19✔
104
            self.content = encoding.encode(&buffer)?.into();
19✔
NEW
105
        } else {
×
NEW
106
            self.content = buffer.into();
×
NEW
107
        }
×
108

109
        self.into_signatures_builder()
19✔
110
    }
19✔
111

112
    /// Set the provided JSON content, applying already set `content-encoding`.
113
    ///
114
    /// # Errors
115
    ///  - Verifies that `content-type` field is set to JSON
116
    ///  - Cannot serialize provided JSON
117
    ///  - Compression failure
118
    pub fn with_json_content(
446✔
119
        mut self,
446✔
120
        json: &serde_json::Value,
446✔
121
    ) -> anyhow::Result<SignaturesBuilder> {
446✔
122
        anyhow::ensure!(
446✔
123
            self.metadata.content_type() == Some(ContentType::Json)
446✔
124
                || self.metadata.content_type() == Some(ContentType::SchemaJson),
187✔
125
            "Already set metadata field `content-type` is not JSON value"
×
126
        );
127

128
        let content = serde_json::to_vec(&json)?;
446✔
129
        if let Some(encoding) = self.metadata.content_encoding() {
446✔
130
            self.content = encoding.encode(&content)?.into();
363✔
131
        } else {
83✔
132
            self.content = content.into();
83✔
133
        }
83✔
134

135
        self.into_signatures_builder()
446✔
136
    }
446✔
137
}
138

139
impl SignaturesBuilder {
140
    /// Add a signature to the document
141
    ///
142
    /// # Errors
143
    ///
144
    /// Fails if a `CatalystSignedDocument` cannot be created due to missing metadata or
145
    /// content, due to malformed data, or when the signed document cannot be
146
    /// converted into `coset::CoseSign`.
147
    pub fn add_signature(
400✔
148
        mut self,
400✔
149
        sign_fn: impl FnOnce(Vec<u8>) -> Vec<u8>,
400✔
150
        kid: CatalystId,
400✔
151
    ) -> anyhow::Result<Self> {
400✔
152
        if kid.is_id() {
400✔
153
            anyhow::bail!("Provided kid should be in a uri format, kid: {kid}");
×
154
        }
400✔
155

156
        self.signatures.push(build_signature(
400✔
157
            sign_fn,
400✔
158
            kid,
400✔
159
            &self.metadata,
400✔
160
            &self.content,
400✔
161
        )?);
×
162

163
        Ok(self)
400✔
164
    }
400✔
165

166
    /// Builds a document from the set `metadata`, `content` and `signatures`.
167
    ///
168
    /// # Errors:
169
    ///  - CBOR encoding/decoding failures
170
    pub fn build(self) -> anyhow::Result<CatalystSignedDocument> {
481✔
171
        let metadata_bytes = minicbor::to_vec(&self.metadata)?;
481✔
172
        let content_bytes = minicbor::to_vec(&self.content)?;
481✔
173
        let signature_bytes = minicbor::to_vec(&self.signatures)?;
481✔
174
        let doc = build_document(&metadata_bytes, &content_bytes, &signature_bytes)?;
481✔
175
        Ok(doc)
481✔
176
    }
481✔
177
}
178

179
/// Build document from the provided **CBOR encoded** `metadata`, `content` and
180
/// `signatures`.
181
fn build_document(
697✔
182
    metadata_bytes: &[u8],
697✔
183
    content_bytes: &[u8],
697✔
184
    signatures_bytes: &[u8],
697✔
185
) -> anyhow::Result<CatalystSignedDocument> {
697✔
186
    let mut e = minicbor::Encoder::new(Vec::new());
697✔
187
    // COSE_Sign tag
188
    // <!https://datatracker.ietf.org/doc/html/rfc8152#page-9>
189
    e.tag(minicbor::data::Tag::new(98))?;
697✔
190
    e.array(4)?;
697✔
191
    // protected headers (metadata fields)
192
    e.bytes(metadata_bytes)?;
697✔
193
    // empty unprotected headers
194
    e.map(0)?;
697✔
195
    // content
196
    e.writer_mut().write_all(content_bytes)?;
697✔
197
    // signatures
198
    e.writer_mut().write_all(signatures_bytes)?;
697✔
199
    CatalystSignedDocument::try_from(e.into_writer().as_slice())
697✔
200
}
697✔
201

202
/// Builds a `Signature` object by signing provided `metadata_bytes`, `content_bytes` and
203
/// `kid` params.
204
fn build_signature(
437✔
205
    sign_fn: impl FnOnce(Vec<u8>) -> Vec<u8>,
437✔
206
    kid: CatalystId,
437✔
207
    metadata: &WithCborBytes<Metadata>,
437✔
208
    content: &Content,
437✔
209
) -> anyhow::Result<Signature> {
437✔
210
    let data_to_sign = tbs_data(&kid, metadata, content)?;
437✔
211
    let sign_bytes = sign_fn(data_to_sign);
437✔
212
    Ok(Signature::new(kid, sign_bytes))
437✔
213
}
437✔
214

215
impl TryFrom<&CatalystSignedDocument> for SignaturesBuilder {
216
    type Error = anyhow::Error;
217

218
    fn try_from(value: &CatalystSignedDocument) -> Result<Self, Self::Error> {
1✔
219
        Ok(Self {
1✔
220
            metadata: value.inner.metadata.clone(),
1✔
221
            content: value.inner.content.clone(),
1✔
222
            signatures: value.inner.signatures.clone(),
1✔
223
        })
1✔
224
    }
1✔
225
}
226

227
#[cfg(test)]
228
pub(crate) mod tests {
229
    use cbork_utils::with_cbor_bytes::WithCborBytes;
230

231
    /// A test version of the builder, which allows to build a not fully valid catalyst
232
    /// signed document
233
    #[derive(Default)]
234
    pub(crate) struct Builder {
235
        /// metadata
236
        metadata: super::Metadata,
237
        /// content
238
        content: super::Content,
239
        /// signatures
240
        signatures: super::Signatures,
241
    }
242

243
    impl Builder {
244
        /// Start building a signed document
245
        #[must_use]
246
        pub(crate) fn new() -> Self {
216✔
247
            Self::default()
216✔
248
        }
216✔
249

250
        /// Add provided `SupportedField` into the `Metadata`.
251
        pub(crate) fn with_metadata_field(
430✔
252
            mut self,
430✔
253
            field: crate::metadata::SupportedField,
430✔
254
        ) -> Self {
430✔
255
            self.metadata.add_field(field);
430✔
256
            self
430✔
257
        }
430✔
258

259
        /// Set the content (COSE payload) to the document builder.
260
        /// It will set the content as its provided, make sure by yourself that
261
        /// `content-type` and `content-encoding` fields are aligned with the
262
        /// provided content bytes.
263
        pub(crate) fn with_content(
33✔
264
            mut self,
33✔
265
            content: Vec<u8>,
33✔
266
        ) -> Self {
33✔
267
            self.content = content.into();
33✔
268
            self
33✔
269
        }
33✔
270

271
        /// Add a signature to the document
272
        pub(crate) fn add_signature(
37✔
273
            mut self,
37✔
274
            sign_fn: impl FnOnce(Vec<u8>) -> Vec<u8>,
37✔
275
            kid: super::CatalystId,
37✔
276
        ) -> anyhow::Result<Self> {
37✔
277
            let metadata = WithCborBytes::new(self.metadata, &mut ())?;
37✔
278
            self.signatures.push(super::build_signature(
37✔
279
                sign_fn,
37✔
280
                kid,
37✔
281
                &metadata,
37✔
282
                &self.content,
37✔
283
            )?);
×
284
            self.metadata = metadata.inner();
37✔
285
            Ok(self)
37✔
286
        }
37✔
287

288
        /// Build a signed document with the collected error report.
289
        /// Could provide an invalid document.
290
        pub(crate) fn build(self) -> super::CatalystSignedDocument {
216✔
291
            let metadata_bytes = minicbor::to_vec(self.metadata).unwrap();
216✔
292
            let content_bytes = minicbor::to_vec(self.content).unwrap();
216✔
293
            let signature_bytes = minicbor::to_vec(self.signatures).unwrap();
216✔
294
            super::build_document(&metadata_bytes, &content_bytes, &signature_bytes).unwrap()
216✔
295
        }
216✔
296
    }
297
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc