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

input-output-hk / catalyst-libs / 20020618668

08 Dec 2025 07:49AM UTC coverage: 67.769% (+0.01%) from 67.756%
20020618668

Pull #677

github

web-flow
Merge 0c764ec7c into 9f3f3a36b
Pull Request #677: feat(rust/signed-doc): Update `link_check` to perform recursive parameters check

132 of 132 new or added lines in 2 files covered. (100.0%)

33 existing lines in 2 files now uncovered.

14075 of 20769 relevant lines covered (67.77%)

2576.39 hits per line

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

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

82
impl From<InnerCatalystSignedDocument> for CatalystSignedDocument {
83
    fn from(inner: InnerCatalystSignedDocument) -> Self {
876✔
84
        Self {
876✔
85
            inner: inner.into(),
876✔
86
        }
876✔
87
    }
876✔
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> {
471✔
98
        self.inner.metadata.doc_type()
471✔
99
    }
471✔
100

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

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

117
    /// Return document content object.
118
    #[must_use]
119
    pub(crate) fn content(&self) -> &Content {
529✔
120
        &self.inner.content
529✔
121
    }
529✔
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>> {
208✔
128
        if let Some(encoding) = self.doc_content_encoding() {
208✔
129
            encoding.decode(self.encoded_content())
167✔
130
        } else {
131
            Ok(self.encoded_content().to_vec())
41✔
132
        }
133
    }
208✔
134

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

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

147
    /// Return document `ContentEncoding`.
148
    #[must_use]
149
    pub fn doc_content_encoding(&self) -> Option<ContentEncoding> {
319✔
150
        self.inner.metadata.content_encoding()
319✔
151
    }
319✔
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,807✔
157
        &self.inner.metadata
1,807✔
158
    }
1,807✔
159

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

166
    /// Return a list of Document's Signer's Catalyst IDs,
167
    #[must_use]
168
    pub fn authors(&self) -> Vec<CatalystId> {
376✔
169
        self.inner
376✔
170
            .signatures
376✔
171
            .iter()
376✔
172
            .map(Signature::kid)
376✔
173
            .cloned()
376✔
174
            .collect()
376✔
175
    }
376✔
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✔
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 {
261✔
217
        self.report().clone()
261✔
218
    }
261✔
219

220
    /// Returns an internal problem report
221
    #[must_use]
222
    pub(crate) fn report(&self) -> &ProblemReport {
1,286✔
223
        &self.inner.report
1,286✔
224
    }
1,286✔
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>> {
1,425✔
241
        let mut e = minicbor::Encoder::new(Vec::new());
1,425✔
242
        self.encode(&mut e, &mut ())?;
1,425✔
243
        Ok(e.into_writer())
1,425✔
244
    }
1,425✔
245

246
    /// Build `CatalystSignedDoc` instance from CBOR bytes.
247
    ///
248
    /// # Errors
249
    ///  - `minicbor::decode::Error`
250
    pub fn from_bytes(
827✔
251
        bytes: &[u8],
827✔
252
        mut policy: CompatibilityPolicy,
827✔
253
    ) -> anyhow::Result<Self> {
827✔
254
        Ok(minicbor::decode_with(bytes, &mut policy)?)
827✔
255
    }
827✔
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> {
1,408✔
271
        let cid = self.to_cid_v1()?;
1,408✔
272
        Ok(DocumentRef::new(
1,408✔
273
            self.doc_id()?,
1,408✔
274
            self.doc_ver()?,
1,408✔
275
            DocLocator::from(cid),
1,408✔
276
        ))
277
    }
1,408✔
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> {
1,410✔
290
        let cbor_bytes = self
1,410✔
291
            .to_bytes()
1,410✔
292
            .map_err(|e| cid_v1::CidError::Encoding(e.to_string()))?;
1,410✔
293
        cid_v1::to_cid_v1(&cbor_bytes)
1,410✔
294
    }
1,410✔
295
}
296

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

307
        let p = d.position();
880✔
308
        if let Ok(tag) = d.tag() {
880✔
309
            if tag != COSE_SIGN_CBOR_TAG {
873✔
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
            }
872✔
314
        } else {
7✔
315
            d.set_position(p);
7✔
316
        }
7✔
317

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

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

333
                // empty unprotected headers
334
                let mut map = cbork_utils::map::Map::decode(
876✔
335
                    &mut minicbor::Decoder::new(headers_bytes.as_slice()),
876✔
336
                    &mut cbork_utils::decode_context::DecodeCtx::Deterministic,
876✔
337
                )?
×
338
                .into_iter();
876✔
339
                if map.next().is_some() {
876✔
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
                }
872✔
346

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

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

357
                InnerCatalystSignedDocument {
876✔
358
                    metadata,
876✔
359
                    content,
876✔
360
                    signatures,
876✔
361
                    report: ctx.into_report(),
876✔
362
                }
876✔
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())
876✔
372
    }
880✔
373
}
374

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

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

404
    fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
827✔
405
        Self::from_bytes(value, CompatibilityPolicy::Accept)
827✔
406
    }
827✔
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