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

input-output-hk / catalyst-libs / 17912317534

22 Sep 2025 10:23AM UTC coverage: 68.574% (-0.2%) from 68.737%
17912317534

Pull #562

github

web-flow
Merge 6d09d01da into 2df110d25
Pull Request #562: feat(rust/signed-doc): Signed Documents integration tests

12 of 13 new or added lines in 3 files covered. (92.31%)

2 existing lines in 1 file now uncovered.

13673 of 19939 relevant lines covered (68.57%)

2955.38 hits per line

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

96.92
/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
    signature::{tbs_data, Signature},
9
    CatalystSignedDocument, Content, ContentType, Metadata, Signatures,
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 {
108✔
47
        Self {
108✔
48
            metadata: Metadata::default(),
108✔
49
        }
108✔
50
    }
108✔
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(
108✔
58
        mut self,
108✔
59
        json: serde_json::Value,
108✔
60
    ) -> anyhow::Result<ContentBuilder> {
108✔
61
        self.metadata = Metadata::from_json(json)?;
108✔
62
        Ok(ContentBuilder {
108✔
63
            metadata: self.metadata,
108✔
64
            content: Content::default(),
108✔
65
        })
108✔
66
    }
108✔
67
}
68

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

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

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

99
        let content = serde_json::to_vec(&json)?;
78✔
100
        if let Some(encoding) = self.metadata.content_encoding() {
78✔
101
            self.content = encoding.encode(&content)?.into();
74✔
102
        } else {
4✔
103
            self.content = content.into();
4✔
104
        }
4✔
105

106
        self.into_signatures_builder()
78✔
107
    }
78✔
108
}
109

110
impl SignaturesBuilder {
111
    /// Add a signature to the document
112
    ///
113
    /// # Errors
114
    ///
115
    /// Fails if a `CatalystSignedDocument` cannot be created due to missing metadata or
116
    /// content, due to malformed data, or when the signed document cannot be
117
    /// converted into `coset::CoseSign`.
118
    pub fn add_signature(
50✔
119
        mut self,
50✔
120
        sign_fn: impl FnOnce(Vec<u8>) -> Vec<u8>,
50✔
121
        kid: CatalystId,
50✔
122
    ) -> anyhow::Result<Self> {
50✔
123
        if kid.is_id() {
50✔
124
            anyhow::bail!("Provided kid should be in a uri format, kid: {kid}");
×
125
        }
50✔
126

127
        self.signatures.push(build_signature(
50✔
128
            sign_fn,
50✔
129
            kid,
50✔
130
            &self.metadata,
50✔
131
            &self.content,
50✔
132
        )?);
×
133

134
        Ok(self)
50✔
135
    }
50✔
136

137
    /// Builds a document from the set `metadata`, `content` and `signatures`.
138
    ///
139
    /// # Errors:
140
    ///  - CBOR encoding/decoding failures
141
    pub fn build(self) -> anyhow::Result<CatalystSignedDocument> {
109✔
142
        let metadata_bytes = minicbor::to_vec(&self.metadata)?;
109✔
143
        let content_bytes = minicbor::to_vec(&self.content)?;
109✔
144
        let signature_bytes = minicbor::to_vec(&self.signatures)?;
109✔
145
        let doc = build_document(&metadata_bytes, &content_bytes, &signature_bytes)?;
109✔
146
        Ok(doc)
109✔
147
    }
109✔
148
}
149

150
/// Build document from the provided **CBOR encoded** `metadata`, `content` and
151
/// `signatures`.
152
fn build_document(
283✔
153
    metadata_bytes: &[u8],
283✔
154
    content_bytes: &[u8],
283✔
155
    signatures_bytes: &[u8],
283✔
156
) -> anyhow::Result<CatalystSignedDocument> {
283✔
157
    let mut e = minicbor::Encoder::new(Vec::new());
283✔
158
    // COSE_Sign tag
159
    // <!https://datatracker.ietf.org/doc/html/rfc8152#page-9>
160
    e.tag(minicbor::data::Tag::new(98))?;
283✔
161
    e.array(4)?;
283✔
162
    // protected headers (metadata fields)
163
    e.bytes(metadata_bytes)?;
283✔
164
    // empty unprotected headers
165
    e.map(0)?;
283✔
166
    // content
167
    e.writer_mut().write_all(content_bytes)?;
283✔
168
    // signatures
169
    e.writer_mut().write_all(signatures_bytes)?;
283✔
170
    CatalystSignedDocument::try_from(e.into_writer().as_slice())
283✔
171
}
283✔
172

173
/// Builds a `Signature` object by signing provided `metadata_bytes`, `content_bytes` and
174
/// `kid` params.
175
fn build_signature(
63✔
176
    sign_fn: impl FnOnce(Vec<u8>) -> Vec<u8>,
63✔
177
    kid: CatalystId,
63✔
178
    metadata: &WithCborBytes<Metadata>,
63✔
179
    content: &Content,
63✔
180
) -> anyhow::Result<Signature> {
63✔
181
    let data_to_sign = tbs_data(&kid, metadata, content)?;
63✔
182
    let sign_bytes = sign_fn(data_to_sign);
63✔
183
    Ok(Signature::new(kid, sign_bytes))
63✔
184
}
63✔
185

186
impl TryFrom<&CatalystSignedDocument> for SignaturesBuilder {
187
    type Error = anyhow::Error;
188

189
    fn try_from(value: &CatalystSignedDocument) -> Result<Self, Self::Error> {
1✔
190
        Ok(Self {
1✔
191
            metadata: value.inner.metadata.clone(),
1✔
192
            content: value.inner.content.clone(),
1✔
193
            signatures: value.inner.signatures.clone(),
1✔
194
        })
1✔
195
    }
1✔
196
}
197

198
#[cfg(test)]
199
pub(crate) mod tests {
200
    use cbork_utils::with_cbor_bytes::WithCborBytes;
201

202
    /// A test version of the builder, which allows to build a not fully valid catalyst
203
    /// signed document
204
    #[derive(Default)]
205
    pub(crate) struct Builder {
206
        /// metadata
207
        metadata: super::Metadata,
208
        /// content
209
        content: super::Content,
210
        /// signatures
211
        signatures: super::Signatures,
212
    }
213

214
    impl Builder {
215
        /// Start building a signed document
216
        #[must_use]
217
        pub(crate) fn new() -> Self {
174✔
218
            Self::default()
174✔
219
        }
174✔
220

221
        /// Add provided `SupportedField` into the `Metadata`.
222
        pub(crate) fn with_metadata_field(
320✔
223
            mut self,
320✔
224
            field: crate::metadata::SupportedField,
320✔
225
        ) -> Self {
320✔
226
            self.metadata.add_field(field);
320✔
227
            self
320✔
228
        }
320✔
229

230
        /// Set the content (COSE payload) to the document builder.
231
        /// It will set the content as its provided, make sure by yourself that
232
        /// `content-type` and `content-encoding` fields are aligned with the
233
        /// provided content bytes.
234
        pub(crate) fn with_content(
37✔
235
            mut self,
37✔
236
            content: Vec<u8>,
37✔
237
        ) -> Self {
37✔
238
            self.content = content.into();
37✔
239
            self
37✔
240
        }
37✔
241

242
        /// Add a signature to the document
243
        pub(crate) fn add_signature(
13✔
244
            mut self,
13✔
245
            sign_fn: impl FnOnce(Vec<u8>) -> Vec<u8>,
13✔
246
            kid: super::CatalystId,
13✔
247
        ) -> anyhow::Result<Self> {
13✔
248
            let metadata = WithCborBytes::new(self.metadata, &mut ())?;
13✔
249
            self.signatures.push(super::build_signature(
13✔
250
                sign_fn,
13✔
251
                kid,
13✔
252
                &metadata,
13✔
253
                &self.content,
13✔
254
            )?);
×
255
            self.metadata = metadata.inner();
13✔
256
            Ok(self)
13✔
257
        }
13✔
258

259
        /// Build a signed document with the collected error report.
260
        /// Could provide an invalid document.
261
        pub(crate) fn build(self) -> super::CatalystSignedDocument {
174✔
262
            let metadata_bytes = minicbor::to_vec(self.metadata).unwrap();
174✔
263
            let content_bytes = minicbor::to_vec(self.content).unwrap();
174✔
264
            let signature_bytes = minicbor::to_vec(self.signatures).unwrap();
174✔
265
            super::build_document(&metadata_bytes, &content_bytes, &signature_bytes).unwrap()
174✔
266
        }
174✔
267
    }
268
}
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