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

input-output-hk / catalyst-libs / 15142932477

20 May 2025 04:29PM UTC coverage: 65.619% (-0.02%) from 65.637%
15142932477

Pull #341

github

web-flow
Merge 2971d7241 into 34094d483
Pull Request #341: feat(docs): signed doc spec 0.04

10944 of 16678 relevant lines covered (65.62%)

2441.02 hits per line

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

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

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

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

17
use anyhow::Context;
18
pub use builder::Builder;
19
pub use catalyst_types::{
20
    problem_report::ProblemReport,
21
    uuid::{Uuid, UuidV4, UuidV7},
22
};
23
pub use content::Content;
24
use coset::{CborSerializable, Header, TaggedCborSerializable};
25
pub use metadata::{ContentEncoding, ContentType, DocumentRef, ExtraFields, Metadata, Section};
26
use minicbor::{decode, encode, Decode, Decoder, Encode};
27
pub use signature::{CatalystId, Signatures};
28

29
/// A problem report content string
30
const PROBLEM_REPORT_CTX: &str = "Catalyst Signed Document";
31

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

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

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

72
impl From<InnerCatalystSignedDocument> for CatalystSignedDocument {
73
    fn from(inner: InnerCatalystSignedDocument) -> Self {
110✔
74
        Self {
110✔
75
            inner: inner.into(),
110✔
76
        }
110✔
77
    }
110✔
78
}
79

80
impl CatalystSignedDocument {
81
    // A bunch of getters to access the contents, or reason through the document, such as.
82

83
    /// Return Document Type `UUIDv4`.
84
    ///
85
    /// # Errors
86
    /// - Missing 'type' field.
87
    pub fn doc_type(&self) -> anyhow::Result<UuidV4> {
42✔
88
        self.inner.metadata.doc_type()
42✔
89
    }
42✔
90

91
    /// Return Document ID `UUIDv7`.
92
    ///
93
    /// # Errors
94
    /// - Missing 'id' field.
95
    pub fn doc_id(&self) -> anyhow::Result<UuidV7> {
43✔
96
        self.inner.metadata.doc_id()
43✔
97
    }
43✔
98

99
    /// Return Document Version `UUIDv7`.
100
    ///
101
    /// # Errors
102
    /// - Missing 'ver' field.
103
    pub fn doc_ver(&self) -> anyhow::Result<UuidV7> {
16✔
104
        self.inner.metadata.doc_ver()
16✔
105
    }
16✔
106

107
    /// Return document `Content`.
108
    #[must_use]
109
    pub fn doc_content(&self) -> &Content {
39✔
110
        &self.inner.content
39✔
111
    }
39✔
112

113
    /// Return document `ContentType`.
114
    ///
115
    /// # Errors
116
    /// - Missing 'content-type' field.
117
    pub fn doc_content_type(&self) -> anyhow::Result<ContentType> {
26✔
118
        self.inner.metadata.content_type()
26✔
119
    }
26✔
120

121
    /// Return document `ContentEncoding`.
122
    #[must_use]
123
    pub fn doc_content_encoding(&self) -> Option<ContentEncoding> {
14✔
124
        self.inner.metadata.content_encoding()
14✔
125
    }
14✔
126

127
    /// Return document metadata content.
128
    #[must_use]
129
    pub fn doc_meta(&self) -> &ExtraFields {
118✔
130
        self.inner.metadata.extra()
118✔
131
    }
118✔
132

133
    /// Return a Document's signatures
134
    #[must_use]
135
    pub(crate) fn signatures(&self) -> &Signatures {
×
136
        &self.inner.signatures
×
137
    }
×
138

139
    /// Return a list of Document's Catalyst IDs.
140
    #[must_use]
141
    pub fn kids(&self) -> Vec<CatalystId> {
13✔
142
        self.inner.signatures.kids()
13✔
143
    }
13✔
144

145
    /// Return a list of Document's author IDs (short form of Catalyst IDs).
146
    #[must_use]
147
    pub fn authors(&self) -> Vec<CatalystId> {
×
148
        self.inner.signatures.authors()
×
149
    }
×
150

151
    /// Returns a collected problem report for the document.
152
    /// It accumulates all kind of errors, collected during the decoding, type based
153
    /// validation and signature verification.
154
    ///
155
    /// This is method is only for the public API usage, do not use it internally inside
156
    /// this crate.
157
    #[must_use]
158
    pub fn problem_report(&self) -> ProblemReport {
10✔
159
        self.report().clone()
10✔
160
    }
10✔
161

162
    /// Returns an internal problem report
163
    #[must_use]
164
    pub(crate) fn report(&self) -> &ProblemReport {
127✔
165
        &self.inner.report
127✔
166
    }
127✔
167

168
    /// Convert Catalyst Signed Document into `coset::CoseSign`
169
    ///
170
    /// # Errors
171
    /// Could fails if the `CatalystSignedDocument` object is not valid.
172
    pub(crate) fn as_cose_sign(&self) -> anyhow::Result<coset::CoseSign> {
9✔
173
        self.inner.as_cose_sign()
9✔
174
    }
9✔
175

176
    /// Returns a signed document `Builder` pre-loaded with the current signed document's
177
    /// data.
178
    #[must_use]
179
    pub fn into_builder(&self) -> Builder {
×
180
        self.into()
×
181
    }
×
182
}
183

184
impl InnerCatalystSignedDocument {
185
    /// Convert Catalyst Signed Document into `coset::CoseSign`
186
    ///
187
    /// # Errors
188
    /// Could fails if the `CatalystSignedDocument` object is not valid.
189
    fn as_cose_sign(&self) -> anyhow::Result<coset::CoseSign> {
27✔
190
        let protected_header =
27✔
191
            Header::try_from(&self.metadata).context("Failed to encode Document Metadata")?;
27✔
192

193
        let content = self
27✔
194
            .content
27✔
195
            .encoded_bytes(self.metadata.content_encoding())?;
27✔
196

197
        let mut builder = coset::CoseSignBuilder::new()
27✔
198
            .protected(protected_header)
27✔
199
            .payload(content);
27✔
200

201
        for signature in self.signatures.cose_signatures() {
27✔
202
            builder = builder.add_signature(signature);
18✔
203
        }
18✔
204
        Ok(builder.build())
27✔
205
    }
27✔
206
}
207

208
impl Decode<'_, ()> for CatalystSignedDocument {
209
    fn decode(d: &mut Decoder<'_>, _ctx: &mut ()) -> Result<Self, decode::Error> {
6✔
210
        let start = d.position();
6✔
211
        d.skip()?;
6✔
212
        let end = d.position();
6✔
213
        let cose_bytes = d
6✔
214
            .input()
6✔
215
            .get(start..end)
6✔
216
            .ok_or(minicbor::decode::Error::end_of_input())?;
6✔
217

218
        let cose_sign = coset::CoseSign::from_tagged_slice(cose_bytes)
6✔
219
            .or_else(|_| coset::CoseSign::from_slice(cose_bytes))
6✔
220
            .map_err(|e| {
6✔
221
                minicbor::decode::Error::message(format!("Invalid COSE Sign document: {e}"))
×
222
            })?;
6✔
223

224
        let report = ProblemReport::new(PROBLEM_REPORT_CTX);
6✔
225
        let metadata = Metadata::from_protected_header(&cose_sign.protected, &report);
6✔
226
        let signatures = Signatures::from_cose_sig_list(&cose_sign.signatures, &report);
6✔
227

228
        let content = if let Some(payload) = cose_sign.payload {
6✔
229
            Content::from_encoded(payload, metadata.content_encoding(), &report)
6✔
230
        } else {
231
            report.missing_field("COSE Sign Payload", "Missing document content (payload)");
×
232
            Content::default()
×
233
        };
234

235
        Ok(InnerCatalystSignedDocument {
6✔
236
            metadata,
6✔
237
            content,
6✔
238
            signatures,
6✔
239
            report,
6✔
240
        }
6✔
241
        .into())
6✔
242
    }
6✔
243
}
244

245
impl Encode<()> for CatalystSignedDocument {
246
    fn encode<W: minicbor::encode::Write>(
2✔
247
        &self, e: &mut encode::Encoder<W>, _ctx: &mut (),
2✔
248
    ) -> Result<(), encode::Error<W::Error>> {
2✔
249
        let cose_sign = self.as_cose_sign().map_err(encode::Error::message)?;
2✔
250

251
        let cose_bytes = cose_sign.to_tagged_vec().map_err(|e| {
2✔
252
            minicbor::encode::Error::message(format!("Failed to encode COSE Sign document: {e}"))
×
253
        })?;
2✔
254

255
        e.writer_mut()
2✔
256
            .write_all(&cose_bytes)
2✔
257
            .map_err(|_| minicbor::encode::Error::message("Failed to encode to CBOR"))
2✔
258
    }
2✔
259
}
260

261
impl TryFrom<&[u8]> for CatalystSignedDocument {
262
    type Error = anyhow::Error;
263

264
    fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
6✔
265
        Ok(minicbor::decode(value)?)
6✔
266
    }
6✔
267
}
268

269
impl TryFrom<CatalystSignedDocument> for Vec<u8> {
270
    type Error = anyhow::Error;
271

272
    fn try_from(value: CatalystSignedDocument) -> Result<Self, Self::Error> {
2✔
273
        Ok(minicbor::to_vec(value)?)
2✔
274
    }
2✔
275
}
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