• 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

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

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

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

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

32
use crate::{builder::SignaturesBuilder, metadata::SupportedLabel};
33

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

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

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

61
impl Display for CatalystSignedDocument {
62
    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
×
NEW
63
        self.inner.metadata.fmt(f)?;
×
64
        writeln!(f, "Payload Size: {} bytes", self.inner.content.size())?;
×
65
        writeln!(f, "Signature Information")?;
×
66
        if self.inner.signatures.is_empty() {
×
67
            writeln!(f, "  This document is unsigned.")?;
×
68
        } else {
NEW
69
            for kid in &self.kids() {
×
70
                writeln!(f, "  Signature Key ID: {kid}")?;
×
71
            }
72
        }
73
        Ok(())
×
74
    }
×
75
}
76

77
impl From<InnerCatalystSignedDocument> for CatalystSignedDocument {
78
    fn from(inner: InnerCatalystSignedDocument) -> Self {
204✔
79
        Self {
204✔
80
            inner: inner.into(),
204✔
81
        }
204✔
82
    }
204✔
83
}
84

85
impl CatalystSignedDocument {
86
    // A bunch of getters to access the contents, or reason through the document, such as.
87

88
    /// Return Document Type `DocType` - List of `UUIDv4`.
89
    ///
90
    /// # Errors
91
    /// - Missing 'type' field.
92
    pub fn doc_type(&self) -> anyhow::Result<&DocType> {
75✔
93
        self.inner.metadata.doc_type()
75✔
94
    }
75✔
95

96
    /// Return Document ID `UUIDv7`.
97
    ///
98
    /// # Errors
99
    /// - Missing 'id' field.
100
    pub fn doc_id(&self) -> anyhow::Result<UuidV7> {
211✔
101
        self.inner.metadata.doc_id()
211✔
102
    }
211✔
103

104
    /// Return Document Version `UUIDv7`.
105
    ///
106
    /// # Errors
107
    /// - Missing 'ver' field.
108
    pub fn doc_ver(&self) -> anyhow::Result<UuidV7> {
211✔
109
        self.inner.metadata.doc_ver()
211✔
110
    }
211✔
111

112
    /// Return document content object.
113
    #[must_use]
114
    pub(crate) fn content(&self) -> &WithCborBytes<Content> {
96✔
115
        &self.inner.content
96✔
116
    }
96✔
117

118
    /// Return document decoded (original/non compressed) content bytes.
119
    ///
120
    /// # Errors
121
    ///  - Decompression failure
122
    pub fn decoded_content(&self) -> anyhow::Result<Vec<u8>> {
54✔
123
        if let Some(encoding) = self.doc_content_encoding() {
54✔
124
            encoding.decode(self.encoded_content())
33✔
125
        } else {
126
            Ok(self.encoded_content().to_vec())
21✔
127
        }
128
    }
54✔
129

130
    /// Return document encoded (compressed) content bytes.
131
    #[must_use]
132
    pub fn encoded_content(&self) -> &[u8] {
80✔
133
        self.content().bytes()
80✔
134
    }
80✔
135

136
    /// Return document `ContentType`.
137
    ///
138
    /// # Errors
139
    /// - Missing 'content-type' field.
140
    pub fn doc_content_type(&self) -> anyhow::Result<ContentType> {
45✔
141
        self.inner.metadata.content_type()
45✔
142
    }
45✔
143

144
    /// Return document `ContentEncoding`.
145
    #[must_use]
146
    pub fn doc_content_encoding(&self) -> Option<ContentEncoding> {
72✔
147
        self.inner.metadata.content_encoding()
72✔
148
    }
72✔
149

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

157
    /// Return a Document's signatures
158
    #[must_use]
159
    pub(crate) fn signatures(&self) -> &Signatures {
×
160
        &self.inner.signatures
×
161
    }
×
162

163
    /// Return a list of Document's Catalyst IDs.
164
    #[must_use]
165
    pub fn kids(&self) -> Vec<CatalystId> {
21✔
166
        self.inner
21✔
167
            .signatures
21✔
168
            .iter()
21✔
169
            .map(|s| s.kid().clone())
21✔
170
            .collect()
21✔
171
    }
21✔
172

173
    /// Return a list of Document's author IDs (short form of Catalyst IDs).
174
    #[must_use]
175
    pub fn authors(&self) -> Vec<CatalystId> {
×
NEW
176
        self.inner
×
NEW
177
            .signatures
×
NEW
178
            .iter()
×
NEW
179
            .map(|s| s.kid().as_short_id())
×
NEW
180
            .collect()
×
NEW
181
    }
×
182

183
    /// Checks if the CBOR body of the signed doc is in the older version format before
184
    /// v0.04.
185
    ///
186
    /// # Errors
187
    ///
188
    /// Errors from CBOR decoding.
189
    pub fn is_deprecated(&self) -> anyhow::Result<bool> {
9✔
190
        let mut e = minicbor::Encoder::new(Vec::new());
9✔
191

192
        let e = e.encode(self.inner.metadata.clone())?;
9✔
193
        let e = e.to_owned().into_writer();
9✔
194

195
        for entry in cbork_utils::map::Map::decode(
48✔
196
            &mut minicbor::Decoder::new(e.as_slice()),
9✔
197
            &mut cbork_utils::decode_context::DecodeCtx::non_deterministic(),
9✔
NEW
198
        )? {
×
199
            match minicbor::Decoder::new(&entry.key_bytes).decode::<SupportedLabel>()? {
48✔
200
                SupportedLabel::Template
201
                | SupportedLabel::Ref
202
                | SupportedLabel::Reply
203
                | SupportedLabel::Parameters => {
204
                    if DocumentRefs::is_deprecated_cbor(&entry.value)? {
10✔
205
                        return Ok(true);
7✔
206
                    }
3✔
207
                },
208
                _ => {},
38✔
209
            }
210
        }
211

212
        Ok(false)
2✔
213
    }
9✔
214

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

226
    /// Returns an internal problem report
227
    #[must_use]
228
    pub(crate) fn report(&self) -> &ProblemReport {
244✔
229
        &self.inner.report
244✔
230
    }
244✔
231

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

243
impl Decode<'_, CompatibilityPolicy> for CatalystSignedDocument {
244
    fn decode(d: &mut Decoder<'_>, ctx: &mut CompatibilityPolicy) -> Result<Self, decode::Error> {
208✔
245
        let mut ctx = DecodeContext::new(
208✔
246
            *ctx,
208✔
247
            ProblemReport::new("Catalyst Signed Document Decoding"),
208✔
248
        );
249

250
        let p = d.position();
208✔
251
        if let Ok(tag) = d.tag() {
208✔
252
            if tag != COSE_SIGN_CBOR_TAG {
201✔
253
                return Err(minicbor::decode::Error::message(format!(
1✔
254
                    "Must be equal to the COSE_Sign tag value: {COSE_SIGN_CBOR_TAG}"
1✔
255
                )));
1✔
256
            }
200✔
257
        } else {
7✔
258
            d.set_position(p);
7✔
259
        }
7✔
260

261
        let arr = Array::decode(d, &mut DecodeCtx::Deterministic)?;
207✔
262

263
        let signed_doc = match arr.as_slice() {
206✔
264
            [metadata_bytes, headers_bytes, content_bytes, signatures_bytes] => {
206✔
265
                let metadata_bytes = minicbor::Decoder::new(metadata_bytes).bytes()?;
206✔
266
                let metadata = WithCborBytes::<Metadata>::decode(
206✔
267
                    &mut minicbor::Decoder::new(metadata_bytes),
206✔
268
                    &mut ctx,
206✔
269
                )?;
2✔
270

271
                // empty unprotected headers
272
                let mut map = cbork_utils::map::Map::decode(
204✔
273
                    &mut minicbor::Decoder::new(headers_bytes.as_slice()),
204✔
274
                    &mut cbork_utils::decode_context::DecodeCtx::Deterministic,
204✔
NEW
275
                )?
×
276
                .into_iter();
204✔
277
                if map.next().is_some() {
204✔
278
                    ctx.report().unknown_field(
4✔
279
                        "unprotected headers",
4✔
280
                        "non empty unprotected headers",
4✔
281
                        "COSE unprotected headers must be empty",
4✔
282
                    );
4✔
283
                }
200✔
284

285
                let content = WithCborBytes::<Content>::decode(
204✔
286
                    &mut minicbor::Decoder::new(content_bytes.as_slice()),
204✔
287
                    &mut (),
204✔
NEW
288
                )?;
×
289

290
                let signatures = Signatures::decode(
204✔
291
                    &mut minicbor::Decoder::new(signatures_bytes.as_slice()),
204✔
292
                    &mut ctx,
204✔
NEW
293
                )?;
×
294

295
                InnerCatalystSignedDocument {
204✔
296
                    metadata,
204✔
297
                    content,
204✔
298
                    signatures,
204✔
299
                    report: ctx.into_report(),
204✔
300
                }
204✔
301
            },
302
            _ => {
NEW
303
                return Err(minicbor::decode::Error::message(
×
NEW
304
                    "Must be a definite size array of 4 elements",
×
NEW
305
                ));
×
306
            },
307
        };
308

309
        Ok(signed_doc.into())
204✔
310
    }
208✔
311
}
312

313
impl<C> Encode<C> for CatalystSignedDocument {
UNCOV
314
    fn encode<W: minicbor::encode::Write>(
×
NEW
315
        &self, e: &mut encode::Encoder<W>, _ctx: &mut C,
×
UNCOV
316
    ) -> Result<(), encode::Error<W::Error>> {
×
317
        // COSE_Sign tag
318
        // <!https://datatracker.ietf.org/doc/html/rfc8152#page-9>
NEW
319
        e.tag(COSE_SIGN_CBOR_TAG)?;
×
NEW
320
        e.array(4)?;
×
321
        // protected headers (metadata fields)
NEW
322
        e.bytes(
×
NEW
323
            minicbor::to_vec(&self.inner.metadata)
×
NEW
324
                .map_err(minicbor::encode::Error::message)?
×
NEW
325
                .as_slice(),
×
NEW
326
        )?;
×
327
        // empty unprotected headers
NEW
328
        e.map(0)?;
×
329
        // content
NEW
330
        e.encode(&self.inner.content)?;
×
331
        // signatures
NEW
332
        e.encode(&self.inner.signatures)?;
×
NEW
333
        Ok(())
×
UNCOV
334
    }
×
335
}
336

337
impl TryFrom<&[u8]> for CatalystSignedDocument {
338
    type Error = anyhow::Error;
339

340
    fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
156✔
341
        Ok(minicbor::decode_with(
156✔
342
            value,
156✔
343
            &mut CompatibilityPolicy::Accept,
156✔
NEW
344
        )?)
×
345
    }
156✔
346
}
347

348
impl TryFrom<CatalystSignedDocument> for Vec<u8> {
349
    type Error = anyhow::Error;
350

UNCOV
351
    fn try_from(value: CatalystSignedDocument) -> Result<Self, Self::Error> {
×
UNCOV
352
        Ok(minicbor::to_vec(value)?)
×
UNCOV
353
    }
×
354
}
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