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

input-output-hk / catalyst-libs / 16646523141

31 Jul 2025 10:28AM UTC coverage: 67.341% (+3.8%) from 63.544%
16646523141

push

github

web-flow
feat(rust/signed-doc): Implement new Catalyst Signed Doc (#338)

* chore: add new line to open pr

Signed-off-by: bkioshn <bkioshn@gmail.com>

* chore: revert

Signed-off-by: bkioshn <bkioshn@gmail.com>

* feat(rust/signed-doc): add new type `DocType` (#339)

* feat(signed-doc): add new type DocType

Signed-off-by: bkioshn <bkioshn@gmail.com>

* fix(signed-doc): add conversion policy

Signed-off-by: bkioshn <bkioshn@gmail.com>

* fix(signed-doc): doc type

Signed-off-by: bkioshn <bkioshn@gmail.com>

* fix(signed-doc): doc type error

Signed-off-by: bkioshn <bkioshn@gmail.com>

* fix(signed-doc): seperate test

Signed-off-by: bkioshn <bkioshn@gmail.com>

* fix(signed-doc): format

Signed-off-by: bkioshn <bkioshn@gmail.com>

---------

Signed-off-by: bkioshn <bkioshn@gmail.com>

* feat(rust/signed-doc): Add initial decoding tests for the Catalyst Signed Documents (#349)

* wip

* wip

* fix fmt

* fix spelling

* fix clippy

* fix(rust/signed-doc): Apply new `DocType` (#347)

* feat(signed-doc): add new type DocType

Signed-off-by: bkioshn <bkioshn@gmail.com>

* wip: apply doctype

Signed-off-by: bkioshn <bkioshn@gmail.com>

* fix(signed-doc): add more function to DocType

Signed-off-by: bkioshn <bkioshn@gmail.com>

* fix(signed-doc): map old doctype to new doctype

Signed-off-by: bkioshn <bkioshn@gmail.com>

* fix(signed-doc): add eq to uuidv4

Signed-off-by: bkioshn <bkioshn@gmail.com>

* fix(signed-doc): fix validator

Signed-off-by: bkioshn <bkioshn@gmail.com>

* fix(signed-doc): minor fixes

Signed-off-by: bkioshn <bkioshn@gmail.com>

* fix(catalyst-types): add hash to uuidv4

Signed-off-by: bkioshn <bkioshn@gmail.com>

* fix(signed-doc): decoding test

Signed-off-by: bkioshn <bkioshn@gmail.com>

* fix(signed-doc): doctype

Signed-off-by: bkioshn <bkioshn@gmail.com>

* fix(signed-doc): minor fixes

Signed-off-by: bkioshn <bkioshn@gmail.com>

* chore(sign-doc): fix comment

Signed-off-by: bkioshn <bkioshn@gmail.com>

* fix(catalyst-types): add froms... (continued)

2453 of 2675 new or added lines in 38 files covered. (91.7%)

19 existing lines in 7 files now uncovered.

11312 of 16798 relevant lines covered (67.34%)

2525.16 hits per line

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

97.32
/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: WithCborBytes<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 {
55✔
47
        Self {
55✔
48
            metadata: Metadata::default(),
55✔
49
        }
55✔
50
    }
55✔
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(mut self, json: serde_json::Value) -> anyhow::Result<ContentBuilder> {
55✔
58
        self.metadata = Metadata::from_json(json)?;
55✔
59
        Ok(ContentBuilder {
55✔
60
            metadata: self.metadata,
55✔
61
            content: Content::default(),
55✔
62
        })
55✔
63
    }
55✔
64
}
65

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

76
    /// Sets an empty content
77
    pub fn empty_content(self) -> anyhow::Result<SignaturesBuilder> {
24✔
78
        self.into_signatures_builder()
24✔
79
    }
24✔
80

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

95
        let content = serde_json::to_vec(&json)?;
31✔
96
        if let Some(encoding) = self.metadata.content_encoding() {
31✔
97
            self.content = encoding.encode(&content)?.into();
30✔
98
        } else {
1✔
99
            self.content = content.into();
1✔
100
        }
1✔
101

102
        self.into_signatures_builder()
31✔
103
    }
31✔
104
}
105

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

121
        self.signatures.push(build_signature(
11✔
122
            sign_fn,
11✔
123
            kid,
11✔
124
            &self.metadata,
11✔
125
            &self.content,
11✔
NEW
126
        )?);
×
127

128
        Ok(self)
11✔
129
    }
11✔
130

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

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

165
/// Builds a `Signature` object by signing provided `metadata_bytes`, `content_bytes` and
166
/// `kid` params.
167
fn build_signature(
12✔
168
    sign_fn: impl FnOnce(Vec<u8>) -> Vec<u8>, kid: CatalystId, metadata: &WithCborBytes<Metadata>,
12✔
169
    content: &WithCborBytes<Content>,
12✔
170
) -> anyhow::Result<Signature> {
12✔
171
    let data_to_sign = tbs_data(&kid, metadata, content)?;
12✔
172
    let sign_bytes = sign_fn(data_to_sign);
12✔
173
    Ok(Signature::new(kid, sign_bytes))
12✔
174
}
12✔
175

176
impl TryFrom<&CatalystSignedDocument> for SignaturesBuilder {
177
    type Error = anyhow::Error;
178

179
    fn try_from(value: &CatalystSignedDocument) -> Result<Self, Self::Error> {
1✔
180
        Ok(Self {
1✔
181
            metadata: value.inner.metadata.clone(),
1✔
182
            content: value.inner.content.clone(),
1✔
183
            signatures: value.inner.signatures.clone(),
1✔
184
        })
1✔
185
    }
1✔
186
}
187

188
#[cfg(test)]
189
pub(crate) mod tests {
190
    use cbork_utils::with_cbor_bytes::WithCborBytes;
191

192
    /// A test version of the builder, which allows to build a not fully valid catalyst
193
    /// signed document
194
    #[derive(Default)]
195
    pub(crate) struct Builder {
196
        /// metadata
197
        metadata: super::Metadata,
198
        /// content
199
        content: super::Content,
200
        /// signatures
201
        signatures: super::Signatures,
202
    }
203

204
    impl Builder {
205
        /// Start building a signed document
206
        #[must_use]
207
        pub(crate) fn new() -> Self {
95✔
208
            Self::default()
95✔
209
        }
95✔
210

211
        /// Add provided `SupportedField` into the `Metadata`.
212
        pub(crate) fn with_metadata_field(
142✔
213
            mut self, field: crate::metadata::SupportedField,
142✔
214
        ) -> Self {
142✔
215
            self.metadata.add_field(field);
142✔
216
            self
142✔
217
        }
142✔
218

219
        /// Set the content (COSE payload) to the document builder.
220
        /// It will set the content as its provided, make sure by yourself that
221
        /// `content-type` and `content-encoding` fields are aligned with the
222
        /// provided content bytes.
223
        pub(crate) fn with_content(mut self, content: Vec<u8>) -> Self {
25✔
224
            self.content = content.into();
25✔
225
            self
25✔
226
        }
25✔
227

228
        /// Add a signature to the document
229
        pub(crate) fn add_signature(
1✔
230
            mut self, sign_fn: impl FnOnce(Vec<u8>) -> Vec<u8>, kid: super::CatalystId,
1✔
231
        ) -> anyhow::Result<Self> {
1✔
232
            let metadata = WithCborBytes::new(self.metadata, &mut ())?;
1✔
233
            let content = WithCborBytes::new(self.content, &mut ())?;
1✔
234
            self.signatures
1✔
235
                .push(super::build_signature(sign_fn, kid, &metadata, &content)?);
1✔
236
            self.metadata = metadata.inner();
1✔
237
            self.content = content.inner();
1✔
238
            Ok(self)
1✔
239
        }
1✔
240

241
        /// Build a signed document with the collected error report.
242
        /// Could provide an invalid document.
243
        pub(crate) fn build(self) -> super::CatalystSignedDocument {
95✔
244
            let metadata_bytes = minicbor::to_vec(self.metadata).unwrap();
95✔
245
            let content_bytes = minicbor::to_vec(self.content).unwrap();
95✔
246
            let signature_bytes = minicbor::to_vec(self.signatures).unwrap();
95✔
247
            super::build_document(&metadata_bytes, &content_bytes, &signature_bytes).unwrap()
95✔
248
        }
95✔
249
    }
250
}
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

© 2025 Coveralls, Inc