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

input-output-hk / catalyst-libs / 19403595944

16 Nov 2025 09:31AM UTC coverage: 68.206% (-0.09%) from 68.298%
19403595944

push

github

web-flow
feat(rust/signed-doc): CatalystSignedDocument has CIDv1 (#621)

* feat(rust/signed-doc): CatalystSignedDocument has CIDv1

* feat(rust/signed-doc): Cid newtype

* break(rust/signed-doc): DocLocator(Cid) must be valid

* Breaking change that removes compatibility with old versions

* fix(rust/signed-doc): fix unit tests to support DocLocator

* removes support for old version

* fix(rust/signed-doc): fix integration tests

* chore(rust/signed-doc): fmt fix

* chore(rust/signed-doc): fix spelling

* chore(rust/signed-doc): fix doc comment

* chore(rust/signed-doc): Add `doc_ref` method for `CatalystSignedDoc`. (#640)

* update API

* wip

* wip

* wip

* fix test

* wip

* wip

* fix fmt

* fix(rust/signed-doc): remove stale example

---------

Co-authored-by: Joaquín Rosales <joaquin.rosales@iohk.io>

---------

Co-authored-by: Alex Pozhylenkov <leshiy12345678@gmail.com>

525 of 561 new or added lines in 12 files covered. (93.58%)

10 existing lines in 5 files now uncovered.

13841 of 20293 relevant lines covered (68.21%)

2725.97 hits per line

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

92.51
/rust/signed_doc/src/lib.rs
1
//! Catalyst documents signing crate
2

3
mod builder;
4
pub mod cid_v1;
5
mod content;
6
pub mod decode_context;
7
pub mod doc_types;
8
mod metadata;
9
pub mod providers;
10
mod signature;
11
pub mod validator;
12

13
use std::{
14
    convert::TryFrom,
15
    fmt::{Display, Formatter},
16
    sync::Arc,
17
};
18

19
pub use builder::Builder;
20
pub use catalyst_types::{
21
    problem_report::ProblemReport,
22
    uuid::{Uuid, UuidV4, UuidV7},
23
};
24
use cbork_utils::{array::Array, decode_context::DecodeCtx, with_cbor_bytes::WithCborBytes};
25
pub use cid_v1::{Cid, CidError};
26
pub use content::Content;
27
use decode_context::{CompatibilityPolicy, DecodeContext};
28
pub use metadata::{
29
    Chain, ContentEncoding, ContentType, DocLocator, DocType, DocumentRef, DocumentRefs, Metadata,
30
    Section,
31
};
32
use minicbor::{Decode, Decoder, Encode, decode, encode};
33
pub use signature::{CatalystId, Signatures};
34

35
use crate::{builder::SignaturesBuilder, metadata::SupportedLabel, signature::Signature};
36

37
/// `COSE_Sign` object CBOR tag <https://datatracker.ietf.org/doc/html/rfc8152#page-8>
38
const COSE_SIGN_CBOR_TAG: minicbor::data::Tag = minicbor::data::Tag::new(98);
39

40
/// Inner type that holds the Catalyst Signed Document with parsing errors.
41
#[derive(Debug)]
42
struct InnerCatalystSignedDocument {
43
    /// Document Metadata
44
    metadata: WithCborBytes<Metadata>,
45
    /// Document Content
46
    content: Content,
47
    /// Signatures
48
    signatures: Signatures,
49
    /// A comprehensive problem report, which could include a decoding errors along with
50
    /// the other validation errors
51
    report: ProblemReport,
52
}
53

54
/// Keep all the contents private.
55
/// Better even to use a structure like this.  Wrapping in an Arc means we don't have to
56
/// manage the Arc anywhere else. These are likely to be large, best to have the Arc be
57
/// non-optional.
58
#[derive(Debug, Clone)]
59
pub struct CatalystSignedDocument {
60
    /// Catalyst Signed Document metadata, raw doc, with content errors.
61
    inner: Arc<InnerCatalystSignedDocument>,
62
}
63

64
impl Display for CatalystSignedDocument {
65
    fn fmt(
12✔
66
        &self,
12✔
67
        f: &mut Formatter<'_>,
12✔
68
    ) -> Result<(), std::fmt::Error> {
12✔
69
        self.inner.metadata.fmt(f)?;
12✔
70
        writeln!(f, "Signature Information")?;
12✔
71
        if self.inner.signatures.is_empty() {
12✔
72
            writeln!(f, "  This document is unsigned.")?;
12✔
73
        } else {
74
            for kid in &self.authors() {
×
75
                writeln!(f, "  Author ID: {kid}")?;
×
76
            }
77
        }
78
        Ok(())
12✔
79
    }
12✔
80
}
81

82
impl From<InnerCatalystSignedDocument> for CatalystSignedDocument {
83
    fn from(inner: InnerCatalystSignedDocument) -> Self {
668✔
84
        Self {
668✔
85
            inner: inner.into(),
668✔
86
        }
668✔
87
    }
668✔
88
}
89

90
impl CatalystSignedDocument {
91
    // A bunch of getters to access the contents, or reason through the document, such as.
92

93
    /// Return Document Type `DocType` - List of `UUIDv4`.
94
    ///
95
    /// # Errors
96
    /// - Missing 'type' field.
97
    pub fn doc_type(&self) -> anyhow::Result<&DocType> {
345✔
98
        self.inner.metadata.doc_type()
345✔
99
    }
345✔
100

101
    /// Return Document ID `UUIDv7`.
102
    ///
103
    /// # Errors
104
    /// - Missing 'id' field.
105
    pub fn doc_id(&self) -> anyhow::Result<UuidV7> {
1,205✔
106
        self.inner.metadata.doc_id()
1,205✔
107
    }
1,205✔
108

109
    /// Return Document Version `UUIDv7`.
110
    ///
111
    /// # Errors
112
    /// - Missing 'ver' field.
113
    pub fn doc_ver(&self) -> anyhow::Result<UuidV7> {
1,204✔
114
        self.inner.metadata.doc_ver()
1,204✔
115
    }
1,204✔
116

117
    /// Return document content object.
118
    #[must_use]
119
    pub(crate) fn content(&self) -> &Content {
406✔
120
        &self.inner.content
406✔
121
    }
406✔
122

123
    /// Return document decoded (original/non compressed) content bytes.
124
    ///
125
    /// # Errors
126
    ///  - Decompression failure
127
    pub fn decoded_content(&self) -> anyhow::Result<Vec<u8>> {
164✔
128
        if let Some(encoding) = self.doc_content_encoding() {
164✔
129
            encoding.decode(self.encoded_content())
128✔
130
        } else {
131
            Ok(self.encoded_content().to_vec())
36✔
132
        }
133
    }
164✔
134

135
    /// Return document encoded (compressed) content bytes.
136
    #[must_use]
137
    pub fn encoded_content(&self) -> &[u8] {
251✔
138
        self.content().bytes()
251✔
139
    }
251✔
140

141
    /// Return document `ContentType`.
142
    #[must_use]
143
    pub fn doc_content_type(&self) -> Option<ContentType> {
129✔
144
        self.inner.metadata.content_type()
129✔
145
    }
129✔
146

147
    /// Return document `ContentEncoding`.
148
    #[must_use]
149
    pub fn doc_content_encoding(&self) -> Option<ContentEncoding> {
243✔
150
        self.inner.metadata.content_encoding()
243✔
151
    }
243✔
152

153
    /// Return document metadata content.
154
    // TODO: remove this and provide getters from metadata like the rest of its fields have.
155
    #[must_use]
156
    pub fn doc_meta(&self) -> &WithCborBytes<Metadata> {
1,275✔
157
        &self.inner.metadata
1,275✔
158
    }
1,275✔
159

160
    /// Return a Document's signatures
161
    #[must_use]
162
    pub(crate) fn signatures(&self) -> &Signatures {
171✔
163
        &self.inner.signatures
171✔
164
    }
171✔
165

166
    /// Return a list of Document's Signer's Catalyst IDs,
167
    #[must_use]
168
    pub fn authors(&self) -> Vec<CatalystId> {
280✔
169
        self.inner
280✔
170
            .signatures
280✔
171
            .iter()
280✔
172
            .map(Signature::kid)
280✔
173
            .cloned()
280✔
174
            .collect()
280✔
175
    }
280✔
176

177
    /// Checks if the CBOR body of the signed doc is in the older version format before
178
    /// v0.04.
179
    ///
180
    /// # Errors
181
    ///
182
    /// Errors from CBOR decoding.
183
    pub fn is_deprecated(&self) -> anyhow::Result<bool> {
2✔
184
        let mut e = minicbor::Encoder::new(Vec::new());
2✔
185

186
        let e = e.encode(self.inner.metadata.clone())?;
2✔
187
        let e = e.to_owned().into_writer();
2✔
188

189
        for entry in cbork_utils::map::Map::decode(
13✔
190
            &mut minicbor::Decoder::new(e.as_slice()),
2✔
191
            &mut cbork_utils::decode_context::DecodeCtx::non_deterministic(),
2✔
192
        )? {
×
193
            match minicbor::Decoder::new(&entry.key_bytes).decode::<SupportedLabel>()? {
13✔
194
                SupportedLabel::Template
195
                | SupportedLabel::Ref
196
                | SupportedLabel::Reply
197
                | SupportedLabel::Parameters => {
198
                    if DocumentRefs::is_deprecated_cbor(&entry.value)? {
3✔
UNCOV
199
                        return Ok(true);
×
200
                    }
3✔
201
                },
202
                _ => {},
10✔
203
            }
204
        }
205

206
        Ok(false)
2✔
207
    }
2✔
208

209
    /// Returns a collected problem report for the document.
210
    /// It accumulates all kind of errors, collected during the decoding, type based
211
    /// validation and signature verification.
212
    ///
213
    /// This is method is only for the public API usage, do not use it internally inside
214
    /// this crate.
215
    #[must_use]
216
    pub fn problem_report(&self) -> ProblemReport {
197✔
217
        self.report().clone()
197✔
218
    }
197✔
219

220
    /// Returns an internal problem report
221
    #[must_use]
222
    pub(crate) fn report(&self) -> &ProblemReport {
971✔
223
        &self.inner.report
971✔
224
    }
971✔
225

226
    /// Returns a signed document `Builder` pre-loaded with the current signed document's
227
    /// data.
228
    ///
229
    /// # Errors
230
    ///  - If error returned its probably a bug. `CatalystSignedDocument` must be a valid
231
    ///    COSE structure.
232
    pub fn into_builder(&self) -> anyhow::Result<SignaturesBuilder> {
1✔
233
        self.try_into()
1✔
234
    }
1✔
235

236
    /// Returns CBOR bytes.
237
    ///
238
    /// # Errors
239
    ///  - `minicbor::encode::Error`
240
    pub fn to_bytes(&self) -> anyhow::Result<Vec<u8>> {
789✔
241
        let mut e = minicbor::Encoder::new(Vec::new());
789✔
242
        self.encode(&mut e, &mut ())?;
789✔
243
        Ok(e.into_writer())
789✔
244
    }
789✔
245

246
    /// Build `CatalystSignedDoc` instance from CBOR bytes.
247
    ///
248
    /// # Errors
249
    ///  - `minicbor::decode::Error`
250
    pub fn from_bytes(
619✔
251
        bytes: &[u8],
619✔
252
        mut policy: CompatibilityPolicy,
619✔
253
    ) -> anyhow::Result<Self> {
619✔
254
        Ok(minicbor::decode_with(bytes, &mut policy)?)
619✔
255
    }
619✔
256

257
    /// Returns a `DocumentRef` for the current document.
258
    ///
259
    /// Generating a CID v1 (Content Identifier version 1) creates an IPFS-compatible
260
    /// content identifier using:
261
    /// - CID version 1
262
    /// - CBOR multicodec (0x51)
263
    /// - SHA2-256 multihash
264
    ///
265
    /// # Errors
266
    ///  - CBOR serialization failure
267
    ///  - Multihash construction failure
268
    ///  - Missing 'id' field.
269
    ///  - Missing 'ver' field.
270
    pub fn doc_ref(&self) -> anyhow::Result<DocumentRef> {
772✔
271
        let cid = self.to_cid_v1()?;
772✔
272
        Ok(DocumentRef::new(
772✔
273
            self.doc_id()?,
772✔
274
            self.doc_ver()?,
772✔
275
            DocLocator::from(cid),
772✔
276
        ))
277
    }
772✔
278

279
    /// Generate a CID v1 (Content Identifier version 1) for this signed document.
280
    ///
281
    /// Creates an IPFS-compatible content identifier using:
282
    /// - CID version 1
283
    /// - CBOR multicodec (0x51)
284
    /// - SHA2-256 multihash
285
    ///
286
    /// # Errors
287
    ///  - CBOR serialization failure
288
    ///  - Multihash construction failure
289
    fn to_cid_v1(&self) -> Result<cid_v1::Cid, cid_v1::CidError> {
774✔
290
        let cbor_bytes = self
774✔
291
            .to_bytes()
774✔
292
            .map_err(|e| cid_v1::CidError::Encoding(e.to_string()))?;
774✔
293
        cid_v1::to_cid_v1(&cbor_bytes)
774✔
294
    }
774✔
295
}
296

297
impl Decode<'_, CompatibilityPolicy> for CatalystSignedDocument {
298
    fn decode(
672✔
299
        d: &mut Decoder<'_>,
672✔
300
        ctx: &mut CompatibilityPolicy,
672✔
301
    ) -> Result<Self, decode::Error> {
672✔
302
        let mut ctx = DecodeContext::new(
672✔
303
            *ctx,
672✔
304
            ProblemReport::new("Catalyst Signed Document Decoding"),
672✔
305
        );
306

307
        let p = d.position();
672✔
308
        if let Ok(tag) = d.tag() {
672✔
309
            if tag != COSE_SIGN_CBOR_TAG {
665✔
310
                return Err(minicbor::decode::Error::message(format!(
1✔
311
                    "Must be equal to the COSE_Sign tag value: {COSE_SIGN_CBOR_TAG}"
1✔
312
                )));
1✔
313
            }
664✔
314
        } else {
7✔
315
            d.set_position(p);
7✔
316
        }
7✔
317

318
        let arr = Array::decode(d, &mut DecodeCtx::Deterministic)?;
671✔
319

320
        let signed_doc = match arr.as_slice() {
670✔
321
            [
322
                metadata_bytes,
670✔
323
                headers_bytes,
670✔
324
                content_bytes,
670✔
325
                signatures_bytes,
670✔
326
            ] => {
327
                let metadata_bytes = minicbor::Decoder::new(metadata_bytes).bytes()?;
670✔
328
                let metadata = WithCborBytes::<Metadata>::decode(
670✔
329
                    &mut minicbor::Decoder::new(metadata_bytes),
670✔
330
                    &mut ctx,
670✔
331
                )?;
2✔
332

333
                // empty unprotected headers
334
                let mut map = cbork_utils::map::Map::decode(
668✔
335
                    &mut minicbor::Decoder::new(headers_bytes.as_slice()),
668✔
336
                    &mut cbork_utils::decode_context::DecodeCtx::Deterministic,
668✔
337
                )?
×
338
                .into_iter();
668✔
339
                if map.next().is_some() {
668✔
340
                    ctx.report().unknown_field(
4✔
341
                        "unprotected headers",
4✔
342
                        "non empty unprotected headers",
4✔
343
                        "COSE unprotected headers must be empty",
4✔
344
                    );
4✔
345
                }
664✔
346

347
                let content = Content::decode(
668✔
348
                    &mut minicbor::Decoder::new(content_bytes.as_slice()),
668✔
349
                    &mut (),
668✔
350
                )?;
×
351

352
                let signatures = Signatures::decode(
668✔
353
                    &mut minicbor::Decoder::new(signatures_bytes.as_slice()),
668✔
354
                    &mut ctx,
668✔
355
                )?;
×
356

357
                InnerCatalystSignedDocument {
668✔
358
                    metadata,
668✔
359
                    content,
668✔
360
                    signatures,
668✔
361
                    report: ctx.into_report(),
668✔
362
                }
668✔
363
            },
364
            _ => {
365
                return Err(minicbor::decode::Error::message(
×
366
                    "Must be a definite size array of 4 elements",
×
367
                ));
×
368
            },
369
        };
370

371
        Ok(signed_doc.into())
668✔
372
    }
672✔
373
}
374

375
impl<C> Encode<C> for CatalystSignedDocument {
376
    fn encode<W: minicbor::encode::Write>(
789✔
377
        &self,
789✔
378
        e: &mut encode::Encoder<W>,
789✔
379
        _ctx: &mut C,
789✔
380
    ) -> Result<(), encode::Error<W::Error>> {
789✔
381
        // COSE_Sign tag
382
        // <!https://datatracker.ietf.org/doc/html/rfc8152#page-9>
383
        e.tag(COSE_SIGN_CBOR_TAG)?;
789✔
384
        e.array(4)?;
789✔
385
        // protected headers (metadata fields)
386
        e.bytes(
789✔
387
            minicbor::to_vec(&self.inner.metadata)
789✔
388
                .map_err(minicbor::encode::Error::message)?
789✔
389
                .as_slice(),
789✔
390
        )?;
×
391
        // empty unprotected headers
392
        e.map(0)?;
789✔
393
        // content
394
        e.encode(&self.inner.content)?;
789✔
395
        // signatures
396
        e.encode(&self.inner.signatures)?;
789✔
397
        Ok(())
789✔
398
    }
789✔
399
}
400

401
impl TryFrom<&[u8]> for CatalystSignedDocument {
402
    type Error = anyhow::Error;
403

404
    fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
619✔
405
        Self::from_bytes(value, CompatibilityPolicy::Accept)
619✔
406
    }
619✔
407
}
408

409
impl TryFrom<&CatalystSignedDocument> for Vec<u8> {
410
    type Error = anyhow::Error;
411

412
    fn try_from(value: &CatalystSignedDocument) -> Result<Self, Self::Error> {
×
413
        value.to_bytes()
×
414
    }
×
415
}
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