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

input-output-hk / catalyst-libs / 16219415812

11 Jul 2025 11:55AM UTC coverage: 68.835%. First build
16219415812

Pull #418

github

web-flow
Merge b09bdeee6 into a8b219ddd
Pull Request #418: feat(rust/signed-doc): Apply deterministic CBOR decoding from `cbork-utils`

157 of 182 new or added lines in 7 files covered. (86.26%)

12919 of 18768 relevant lines covered (68.84%)

2337.1 hits per line

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

73.51
/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;
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> {
×
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 {
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 {
208✔
79
        Self {
208✔
80
            inner: inner.into(),
208✔
81
        }
208✔
82
    }
208✔
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> {
90✔
93
        self.inner.metadata.doc_type()
90✔
94
    }
90✔
95

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

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

112
    /// Return document content object.
113
    #[must_use]
114
    pub(crate) fn content(&self) -> &WithCborBytes<Content> {
107✔
115
        &self.inner.content
107✔
116
    }
107✔
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>> {
62✔
123
        if let Some(encoding) = self.doc_content_encoding() {
62✔
124
            encoding.decode(self.encoded_content())
41✔
125
        } else {
126
            Ok(self.encoded_content().to_vec())
21✔
127
        }
128
    }
62✔
129

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

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

144
    /// Return document `ContentEncoding`.
145
    #[must_use]
146
    pub fn doc_content_encoding(&self) -> Option<ContentEncoding> {
83✔
147
        self.inner.metadata.content_encoding()
83✔
148
    }
83✔
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> {
333✔
154
        &self.inner.metadata
333✔
155
    }
333✔
156

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

163
    /// Return a list of Document's Catalyst IDs.
164
    #[must_use]
165
    pub fn kids(&self) -> Vec<CatalystId> {
24✔
166
        self.inner
24✔
167
            .signatures
24✔
168
            .iter()
24✔
169
            .map(|s| s.kid().clone())
24✔
170
            .collect()
24✔
171
    }
24✔
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> {
×
176
        self.inner
×
177
            .signatures
×
178
            .iter()
×
179
            .map(|s| s.kid().as_short_id())
×
180
            .collect()
×
181
    }
×
182

183
    /// Returns a collected problem report for the document.
184
    /// It accumulates all kind of errors, collected during the decoding, type based
185
    /// validation and signature verification.
186
    ///
187
    /// This is method is only for the public API usage, do not use it internally inside
188
    /// this crate.
189
    #[must_use]
190
    pub fn problem_report(&self) -> ProblemReport {
43✔
191
        self.report().clone()
43✔
192
    }
43✔
193

194
    /// Returns an internal problem report
195
    #[must_use]
196
    pub(crate) fn report(&self) -> &ProblemReport {
272✔
197
        &self.inner.report
272✔
198
    }
272✔
199

200
    /// Returns a signed document `Builder` pre-loaded with the current signed document's
201
    /// data.
202
    ///
203
    /// # Errors
204
    ///  - If error returned its probably a bug. `CatalystSignedDocument` must be a valid
205
    ///    COSE structure.
206
    pub fn into_builder(&self) -> anyhow::Result<SignaturesBuilder> {
1✔
207
        self.try_into()
1✔
208
    }
1✔
209
}
210

211
impl Decode<'_, CompatibilityPolicy> for CatalystSignedDocument {
212
    fn decode(d: &mut Decoder<'_>, ctx: &mut CompatibilityPolicy) -> Result<Self, decode::Error> {
212✔
213
        let mut ctx = DecodeContext::new(
212✔
214
            *ctx,
212✔
215
            ProblemReport::new("Catalyst Signed Document Decoding"),
212✔
216
        );
212✔
217

212✔
218
        let p = d.position();
212✔
219
        if let Ok(tag) = d.tag() {
212✔
220
            if tag != COSE_SIGN_CBOR_TAG {
205✔
221
                return Err(minicbor::decode::Error::message(format!(
1✔
222
                    "Must be equal to the COSE_Sign tag value: {COSE_SIGN_CBOR_TAG}"
1✔
223
                )));
1✔
224
            }
204✔
225
        } else {
7✔
226
            d.set_position(p);
7✔
227
        }
7✔
228

229
        let arr = Array::decode(d, &mut DecodeCtx::Deterministic)?;
211✔
230

231
        let signed_doc = match arr.as_slice() {
210✔
232
            [metadata_bytes, headers_bytes, content_bytes, signatures_bytes] => {
210✔
233
                let metadata_bytes = minicbor::Decoder::new(metadata_bytes).bytes()?;
210✔
234
                let metadata = WithCborBytes::<Metadata>::decode(
210✔
235
                    &mut minicbor::Decoder::new(metadata_bytes),
210✔
236
                    &mut ctx,
210✔
237
                )?;
210✔
238

239
                // empty unprotected headers
240
                let mut map = cbork_utils::map::Map::decode(
208✔
241
                    &mut minicbor::Decoder::new(headers_bytes.as_slice()),
208✔
242
                    &mut cbork_utils::decode_context::DecodeCtx::Deterministic,
208✔
243
                )?
208✔
244
                .into_iter();
208✔
245
                if map.next().is_some() {
208✔
246
                    ctx.report().unknown_field(
4✔
247
                        "unprotected headers",
4✔
248
                        "non empty unprotected headers",
4✔
249
                        "COSE unprotected headers must be empty",
4✔
250
                    );
4✔
251
                }
204✔
252

253
                let content = WithCborBytes::<Content>::decode(
208✔
254
                    &mut minicbor::Decoder::new(content_bytes.as_slice()),
208✔
255
                    &mut (),
208✔
256
                )?;
208✔
257

258
                let signatures = Signatures::decode(
208✔
259
                    &mut minicbor::Decoder::new(signatures_bytes.as_slice()),
208✔
260
                    &mut ctx,
208✔
261
                )?;
208✔
262

263
                InnerCatalystSignedDocument {
208✔
264
                    metadata,
208✔
265
                    content,
208✔
266
                    signatures,
208✔
267
                    report: ctx.into_report(),
208✔
268
                }
208✔
269
            },
270
            _ => {
NEW
271
                return Err(minicbor::decode::Error::message(
×
NEW
272
                    "Must be a definite size array of 4 elements",
×
NEW
273
                ));
×
274
            },
275
        };
276

277
        Ok(signed_doc.into())
208✔
278
    }
212✔
279
}
280

281
impl<C> Encode<C> for CatalystSignedDocument {
282
    fn encode<W: minicbor::encode::Write>(
×
283
        &self, e: &mut encode::Encoder<W>, _ctx: &mut C,
×
284
    ) -> Result<(), encode::Error<W::Error>> {
×
285
        // COSE_Sign tag
×
286
        // <!https://datatracker.ietf.org/doc/html/rfc8152#page-9>
×
287
        e.tag(COSE_SIGN_CBOR_TAG)?;
×
288
        e.array(4)?;
×
289
        // protected headers (metadata fields)
290
        e.bytes(
×
291
            minicbor::to_vec(&self.inner.metadata)
×
292
                .map_err(minicbor::encode::Error::message)?
×
293
                .as_slice(),
×
294
        )?;
×
295
        // empty unprotected headers
296
        e.map(0)?;
×
297
        // content
298
        e.encode(&self.inner.content)?;
×
299
        // signatures
300
        e.encode(&self.inner.signatures)?;
×
301
        Ok(())
×
302
    }
×
303
}
304

305
impl TryFrom<&[u8]> for CatalystSignedDocument {
306
    type Error = anyhow::Error;
307

308
    fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
167✔
309
        Ok(minicbor::decode_with(
167✔
310
            value,
167✔
311
            &mut CompatibilityPolicy::Accept,
167✔
312
        )?)
167✔
313
    }
167✔
314
}
315

316
impl TryFrom<CatalystSignedDocument> for Vec<u8> {
317
    type Error = anyhow::Error;
318

319
    fn try_from(value: CatalystSignedDocument) -> Result<Self, Self::Error> {
×
320
        Ok(minicbor::to_vec(value)?)
×
321
    }
×
322
}
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