Skip to content

Commit 478f5ab

Browse files
committed
feat(rust/signed-doc): implement TryFrom<Vec<u8>> for CatalystSignedDocument
* updates cat-signed-doc example to display deserialized cose sign documents.
1 parent 9120d04 commit 478f5ab

File tree

2 files changed

+126
-90
lines changed

2 files changed

+126
-90
lines changed

rust/signed_doc/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ anyhow = "1.0.89"
1515
serde = { version = "1.0.210", features = ["derive"] }
1616
serde_json = "1.0"
1717
jsonschema = "0.18.0"
18+
ciborium = "0.2.2"
1819
coset = "0.3.7"
1920
brotli = "7.0.0"
2021
ed25519-dalek = { version = "2.1.1", features = ["pem"] }

rust/signed_doc/src/lib.rs

+125-90
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@ use std::{
88

99
use coset::CborSerializable;
1010

11+
/// Catalyst Signed Document Content Encoding Key.
12+
const CONTENT_ENCODING_KEY: &str = "content encoding";
13+
/// Catalyst Signed Document Content Encoding Value.
14+
const CONTENT_ENCODING_VALUE: &str = "br";
15+
/// CBOR tag for UUID content.
16+
const UUID_CBOR_TAG: u64 = 37;
17+
1118
/// Collection of Content Errors.
1219
pub struct ContentErrors(Vec<String>);
1320

@@ -18,20 +25,22 @@ pub struct ContentErrors(Vec<String>);
1825
pub struct CatalystSignedDocument {
1926
/// Catalyst Signed Document metadata, raw doc, with content errors.
2027
inner: Arc<InnerCatalystSignedDocument>,
21-
/// Content Errors found when parsing the Document
22-
content_errors: Vec<String>,
2328
}
2429

2530
impl Display for CatalystSignedDocument {
2631
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
27-
writeln!(f, "Metadata: {:?}", self.inner.metadata)?;
28-
writeln!(f, "JSON Payload: {}", self.inner.payload)?;
29-
writeln!(f, "Signatures:")?;
32+
writeln!(f, "{}", self.inner.metadata)?;
33+
writeln!(f, "JSON Payload {:#}", self.inner.payload)?;
34+
writeln!(f, "Signatures [")?;
3035
for signature in &self.inner.signatures {
31-
writeln!(f, "\t{}", hex::encode(signature.signature.as_slice()))?;
36+
writeln!(f, " {:#}", hex::encode(signature.signature.as_slice()))?;
37+
}
38+
writeln!(f, "]")?;
39+
writeln!(f, "Content Errors [")?;
40+
for error in &self.inner.content_errors {
41+
writeln!(f, " {error:#}")?;
3242
}
33-
writeln!(f, "Content Errors: {:#?}", self.content_errors)?;
34-
write!(f, "COSE Sign: {:?}", self.inner.cose_sign)
43+
writeln!(f, "]")
3544
}
3645
}
3746

@@ -46,6 +55,8 @@ struct InnerCatalystSignedDocument {
4655
signatures: Vec<coset::CoseSignature>,
4756
/// Raw COSE Sign bytes
4857
cose_sign: coset::CoseSign,
58+
/// Content Errors found when parsing the Document
59+
content_errors: Vec<String>,
4960
}
5061

5162
/// Document Metadata.
@@ -67,6 +78,20 @@ pub struct Metadata {
6778
pub section: Option<String>,
6879
}
6980

81+
impl Display for Metadata {
82+
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
83+
writeln!(f, "Metadata {{")?;
84+
writeln!(f, " doc_type: {},", self.r#type)?;
85+
writeln!(f, " doc_id: {},", self.id)?;
86+
writeln!(f, " doc_ver: {},", self.ver)?;
87+
writeln!(f, " doc_ref: {:?},", self.r#ref)?;
88+
writeln!(f, " doc_template: {:?},", self.template)?;
89+
writeln!(f, " doc_reply: {:?},", self.reply)?;
90+
writeln!(f, " doc_section: {:?}", self.section)?;
91+
writeln!(f, "}}")
92+
}
93+
}
94+
7095
impl Default for Metadata {
7196
fn default() -> Self {
7297
Self {
@@ -106,12 +131,11 @@ pub enum DocumentRef {
106131
impl TryFrom<Vec<u8>> for CatalystSignedDocument {
107132
type Error = anyhow::Error;
108133

109-
#[allow(clippy::todo)]
110134
fn try_from(cose_bytes: Vec<u8>) -> Result<Self, Self::Error> {
111135
let cose = coset::CoseSign::from_slice(&cose_bytes)
112136
.map_err(|e| anyhow::anyhow!("Invalid COSE Sign document: {e}"))?;
113137

114-
let (metadata, content_errors) = metadata_from_cose_protected_header(&cose)?;
138+
let (metadata, content_errors) = metadata_from_cose_protected_header(&cose);
115139
let payload = match &cose.payload {
116140
Some(payload) => {
117141
let mut buf = Vec::new();
@@ -130,10 +154,10 @@ impl TryFrom<Vec<u8>> for CatalystSignedDocument {
130154
payload,
131155
signatures,
132156
cose_sign: cose,
157+
content_errors: content_errors.0,
133158
};
134159
Ok(CatalystSignedDocument {
135160
inner: Arc::new(inner),
136-
content_errors: content_errors.0,
137161
})
138162
}
139163
}
@@ -147,7 +171,7 @@ impl CatalystSignedDocument {
147171
/// Are there any validation errors (as opposed to structural errors.
148172
#[must_use]
149173
pub fn has_error(&self) -> bool {
150-
!self.content_errors.is_empty()
174+
!self.inner.content_errors.is_empty()
151175
}
152176

153177
/// Return Document Type `UUIDv4`.
@@ -161,14 +185,31 @@ impl CatalystSignedDocument {
161185
pub fn doc_id(&self) -> uuid::Uuid {
162186
self.inner.metadata.id
163187
}
164-
}
165188

166-
/// Catalyst Signed Document Content Encoding Key.
167-
const CONTENT_ENCODING_KEY: &str = "content encoding";
168-
/// Catalyst Signed Document Content Encoding Value.
169-
const CONTENT_ENCODING_VALUE: &str = "br";
170-
/// CBOR tag for UUID content.
171-
const UUID_CBOR_TAG: u64 = 37;
189+
/// Return Document Version `UUIDv7`.
190+
#[must_use]
191+
pub fn doc_ver(&self) -> uuid::Uuid {
192+
self.inner.metadata.ver
193+
}
194+
195+
/// Return Last Document Reference `Option<DocumentRef>`.
196+
#[must_use]
197+
pub fn doc_ref(&self) -> Option<DocumentRef> {
198+
self.inner.metadata.r#ref
199+
}
200+
201+
/// Return Document Template `Option<DocumentRef>`.
202+
#[must_use]
203+
pub fn doc_template(&self) -> Option<DocumentRef> {
204+
self.inner.metadata.template
205+
}
206+
207+
/// Return Document Reply `Option<DocumentRef>`.
208+
#[must_use]
209+
pub fn doc_reply(&self) -> Option<DocumentRef> {
210+
self.inner.metadata.reply
211+
}
212+
}
172213

173214
/// Generate the COSE protected header used by Catalyst Signed Document.
174215
fn cose_protected_header() -> coset::Header {
@@ -212,11 +253,19 @@ fn decode_cbor_document_ref(val: &coset::cbor::Value) -> anyhow::Result<Document
212253
}
213254
}
214255

256+
/// Find a value for a given key in the protected header.
257+
fn cose_protected_header_find(cose: &coset::CoseSign, rest_key: &str) -> Option<ciborium::Value> {
258+
cose.protected
259+
.header
260+
.rest
261+
.iter()
262+
.find(|(key, _)| key == &coset::Label::Text(rest_key.to_string()))
263+
.map(|(_, value)| value.clone())
264+
}
265+
215266
/// Extract `Metadata` from `coset::CoseSign`.
216267
#[allow(clippy::too_many_lines)]
217-
fn metadata_from_cose_protected_header(
218-
cose: &coset::CoseSign,
219-
) -> anyhow::Result<(Metadata, ContentErrors)> {
268+
fn metadata_from_cose_protected_header(cose: &coset::CoseSign) -> (Metadata, ContentErrors) {
220269
let expected_header = cose_protected_header();
221270
let mut errors = Vec::new();
222271

@@ -238,15 +287,9 @@ fn metadata_from_cose_protected_header(
238287
}
239288
let mut metadata = Metadata::default();
240289

241-
match cose
242-
.protected
243-
.header
244-
.rest
245-
.iter()
246-
.find(|(key, _)| key == &coset::Label::Text("type".to_string()))
247-
{
248-
Some((_, doc_type)) => {
249-
match decode_cbor_uuid(doc_type) {
290+
match cose_protected_header_find(cose, "type") {
291+
Some(doc_type) => {
292+
match decode_cbor_uuid(&doc_type) {
250293
Ok(doc_type_uuid) => {
251294
if doc_type_uuid.get_version_num() == 4 {
252295
metadata.r#type = doc_type_uuid;
@@ -266,15 +309,9 @@ fn metadata_from_cose_protected_header(
266309
None => errors.push("Invalid COSE protected header, missing `type` field".to_string()),
267310
};
268311

269-
match cose
270-
.protected
271-
.header
272-
.rest
273-
.iter()
274-
.find(|(key, _)| key == &coset::Label::Text("id".to_string()))
275-
{
276-
Some((_, doc_id)) => {
277-
match decode_cbor_uuid(doc_id) {
312+
match cose_protected_header_find(cose, "id") {
313+
Some(doc_id) => {
314+
match decode_cbor_uuid(&doc_id) {
278315
Ok(doc_id_uuid) => {
279316
if doc_id_uuid.get_version_num() == 7 {
280317
metadata.id = doc_id_uuid;
@@ -292,15 +329,9 @@ fn metadata_from_cose_protected_header(
292329
None => errors.push("Invalid COSE protected header, missing `id` field".to_string()),
293330
};
294331

295-
match cose
296-
.protected
297-
.header
298-
.rest
299-
.iter()
300-
.find(|(key, _)| key == &coset::Label::Text("ver".to_string()))
301-
{
302-
Some((_, doc_ver)) => {
303-
match decode_cbor_uuid(doc_ver) {
332+
match cose_protected_header_find(cose, "ver") {
333+
Some(doc_ver) => {
334+
match decode_cbor_uuid(&doc_ver) {
304335
Ok(doc_ver_uuid) => {
305336
let mut is_valid = true;
306337
if doc_ver_uuid.get_version_num() != 7 {
@@ -329,53 +360,57 @@ fn metadata_from_cose_protected_header(
329360
None => errors.push("Invalid COSE protected header, missing `ver` field".to_string()),
330361
}
331362

332-
if let Some((_, value)) = cose
333-
.protected
334-
.header
335-
.rest
336-
.iter()
337-
.find(|(key, _)| key == &coset::Label::Text("ref".to_string()))
338-
{
339-
decode_cbor_document_ref(value)
340-
.map_err(|e| anyhow::anyhow!("Invalid COSE protected header `ref` field, err: {e}"))?;
363+
if let Some(cbor_doc_ref) = cose_protected_header_find(cose, "ref") {
364+
match decode_cbor_document_ref(&cbor_doc_ref) {
365+
Ok(doc_ref) => {
366+
metadata.r#ref = Some(doc_ref);
367+
},
368+
Err(e) => {
369+
errors.push(format!(
370+
"Invalid COSE protected header `ref` field, err: {e}"
371+
));
372+
},
373+
}
341374
}
342375

343-
if let Some((_, value)) = cose
344-
.protected
345-
.header
346-
.rest
347-
.iter()
348-
.find(|(key, _)| key == &coset::Label::Text("template".to_string()))
349-
{
350-
decode_cbor_document_ref(value).map_err(|e| {
351-
anyhow::anyhow!("Invalid COSE protected header `template` field, err: {e}")
352-
})?;
376+
if let Some(cbor_doc_template) = cose_protected_header_find(cose, "template") {
377+
match decode_cbor_document_ref(&cbor_doc_template) {
378+
Ok(doc_template) => {
379+
metadata.template = Some(doc_template);
380+
},
381+
Err(e) => {
382+
errors.push(format!(
383+
"Invalid COSE protected header `template` field, err: {e}"
384+
));
385+
},
386+
}
353387
}
354388

355-
if let Some((_, value)) = cose
356-
.protected
357-
.header
358-
.rest
359-
.iter()
360-
.find(|(key, _)| key == &coset::Label::Text("reply".to_string()))
361-
{
362-
decode_cbor_document_ref(value).map_err(|e| {
363-
anyhow::anyhow!("Invalid COSE protected header `reply` field, err: {e}")
364-
})?;
389+
if let Some(cbor_doc_reply) = cose_protected_header_find(cose, "reply") {
390+
match decode_cbor_document_ref(&cbor_doc_reply) {
391+
Ok(doc_reply) => {
392+
metadata.reply = Some(doc_reply);
393+
},
394+
Err(e) => {
395+
errors.push(format!(
396+
"Invalid COSE protected header `reply` field, err: {e}"
397+
));
398+
},
399+
}
365400
}
366401

367-
if let Some((_, value)) = cose
368-
.protected
369-
.header
370-
.rest
371-
.iter()
372-
.find(|(key, _)| key == &coset::Label::Text("section".to_string()))
373-
{
374-
anyhow::ensure!(
375-
value.is_text(),
376-
"Invalid COSE protected header, missing `section` field"
377-
);
402+
if let Some(cbor_doc_section) = cose_protected_header_find(cose, "section") {
403+
match cbor_doc_section.into_text() {
404+
Ok(doc_section) => {
405+
metadata.section = Some(doc_section);
406+
},
407+
Err(e) => {
408+
errors.push(format!(
409+
"Invalid COSE protected header `section` field, err: {e:?}"
410+
));
411+
},
412+
}
378413
}
379414

380-
Ok((metadata, ContentErrors(errors)))
415+
(metadata, ContentErrors(errors))
381416
}

0 commit comments

Comments
 (0)