Skip to content

Commit c28635d

Browse files
committed
fix(rust/signed_doc): refactor Metadata impl TryFrom<coset::cbor::Value>
1 parent 7e2425d commit c28635d

File tree

2 files changed

+192
-188
lines changed

2 files changed

+192
-188
lines changed

rust/signed_doc/src/lib.rs

Lines changed: 25 additions & 188 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,12 @@ use coset::{CborSerializable, TaggedCborSerializable};
1010

1111
mod metadata;
1212

13-
pub use metadata::{DocumentRef, Metadata};
13+
pub use metadata::{DocumentRef, Metadata, UuidV7};
1414

1515
/// Catalyst Signed Document Content Encoding Key.
1616
const CONTENT_ENCODING_KEY: &str = "content encoding";
1717
/// Catalyst Signed Document Content Encoding Value.
1818
const CONTENT_ENCODING_VALUE: &str = "br";
19-
/// CBOR tag for UUID content.
20-
const UUID_CBOR_TAG: u64 = 37;
21-
22-
/// Collection of Content Errors.
23-
pub struct ContentErrors(Vec<String>);
2419

2520
/// Keep all the contents private.
2621
/// Better even to use a structure like this. Wrapping in an Arc means we don't have to
@@ -75,8 +70,26 @@ impl TryFrom<Vec<u8>> for CatalystSignedDocument {
7570
let cose = coset::CoseSign::from_tagged_slice(&cose_bytes)
7671
.or(coset::CoseSign::from_slice(&cose_bytes))
7772
.map_err(|e| anyhow::anyhow!("Invalid COSE Sign document: {e}"))?;
73+
let mut content_errors = Vec::new();
74+
let expected_header = cose_protected_header();
75+
76+
if cose.protected.header.content_type != expected_header.content_type {
77+
content_errors
78+
.push("Invalid COSE document protected header `content-type` field".to_string());
79+
}
7880

79-
let (metadata, content_errors) = metadata_from_cose_protected_header(&cose);
81+
if !cose.protected.header.rest.iter().any(|(key, value)| {
82+
key == &coset::Label::Text(CONTENT_ENCODING_KEY.to_string())
83+
&& value == &coset::cbor::Value::Text(CONTENT_ENCODING_VALUE.to_string())
84+
}) {
85+
content_errors.push(
86+
"Invalid COSE document protected header {CONTENT_ENCODING_KEY} field".to_string(),
87+
);
88+
}
89+
let metadata = Metadata::from(&cose.protected);
90+
if metadata.has_error() {
91+
content_errors.extend_from_slice(metadata.content_errors());
92+
}
8093
let payload = match &cose.payload {
8194
Some(payload) => {
8295
let mut buf = Vec::new();
@@ -95,7 +108,7 @@ impl TryFrom<Vec<u8>> for CatalystSignedDocument {
95108
payload,
96109
signatures,
97110
cose_sign: cose,
98-
content_errors: content_errors.0,
111+
content_errors,
99112
};
100113
Ok(CatalystSignedDocument {
101114
inner: Arc::new(inner),
@@ -106,7 +119,7 @@ impl TryFrom<Vec<u8>> for CatalystSignedDocument {
106119
impl CatalystSignedDocument {
107120
// A bunch of getters to access the contents, or reason through the document, such as.
108121

109-
/// Are there any validation errors (as opposed to structural errors.
122+
/// Are there any validation errors (as opposed to structural errors).
110123
#[must_use]
111124
pub fn has_error(&self) -> bool {
112125
!self.inner.content_errors.is_empty()
@@ -166,190 +179,14 @@ fn cose_protected_header() -> coset::Header {
166179
.build()
167180
}
168181

169-
/// Decode `CBOR` encoded `UUID`.
170-
fn decode_cbor_uuid(val: &coset::cbor::Value) -> anyhow::Result<uuid::Uuid> {
171-
let Some((UUID_CBOR_TAG, coset::cbor::Value::Bytes(bytes))) = val.as_tag() else {
172-
anyhow::bail!("Invalid CBOR encoded UUID type");
173-
};
174-
let uuid = uuid::Uuid::from_bytes(
175-
bytes
176-
.clone()
177-
.try_into()
178-
.map_err(|_| anyhow::anyhow!("Invalid CBOR encoded UUID type, invalid bytes size"))?,
179-
);
180-
Ok(uuid)
181-
}
182-
183-
/// Decode `CBOR` encoded `DocumentRef`.
184-
#[allow(clippy::indexing_slicing)]
185-
fn decode_cbor_document_ref(val: &coset::cbor::Value) -> anyhow::Result<DocumentRef> {
186-
if let Ok(id) = decode_cbor_uuid(val) {
187-
Ok(DocumentRef::Latest { id })
188-
} else {
189-
let Some(array) = val.as_array() else {
190-
anyhow::bail!("Invalid CBOR encoded document `ref` type");
191-
};
192-
anyhow::ensure!(array.len() == 2, "Invalid CBOR encoded document `ref` type");
193-
let id = decode_cbor_uuid(&array[0])?;
194-
let ver = decode_cbor_uuid(&array[1])?;
195-
Ok(DocumentRef::WithVer(id, ver))
196-
}
197-
}
198-
199182
/// Find a value for a given key in the protected header.
200-
fn cose_protected_header_find(cose: &coset::CoseSign, rest_key: &str) -> Option<ciborium::Value> {
183+
fn cose_protected_header_find(
184+
cose: &coset::CoseSign, rest_key: &str,
185+
) -> Option<coset::cbor::Value> {
201186
cose.protected
202187
.header
203188
.rest
204189
.iter()
205190
.find(|(key, _)| key == &coset::Label::Text(rest_key.to_string()))
206191
.map(|(_, value)| value.clone())
207192
}
208-
209-
/// Extract `Metadata` from `coset::CoseSign`.
210-
#[allow(clippy::too_many_lines)]
211-
fn metadata_from_cose_protected_header(cose: &coset::CoseSign) -> (Metadata, ContentErrors) {
212-
let expected_header = cose_protected_header();
213-
let mut errors = Vec::new();
214-
215-
if cose.protected.header.content_type != expected_header.content_type {
216-
errors.push("Invalid COSE document protected header `content-type` field".to_string());
217-
}
218-
219-
if !cose.protected.header.rest.iter().any(|(key, value)| {
220-
key == &coset::Label::Text(CONTENT_ENCODING_KEY.to_string())
221-
&& value == &coset::cbor::Value::Text(CONTENT_ENCODING_VALUE.to_string())
222-
}) {
223-
errors.push(
224-
"Invalid COSE document protected header {CONTENT_ENCODING_KEY} field".to_string(),
225-
);
226-
}
227-
let mut metadata = Metadata::default();
228-
229-
match cose_protected_header_find(cose, "type") {
230-
Some(doc_type) => {
231-
match decode_cbor_uuid(&doc_type) {
232-
Ok(doc_type_uuid) => {
233-
if doc_type_uuid.get_version_num() == 4 {
234-
metadata.r#type = doc_type_uuid;
235-
} else {
236-
errors.push(format!(
237-
"Document type is not a valid UUIDv4: {doc_type_uuid}"
238-
));
239-
}
240-
},
241-
Err(e) => {
242-
errors.push(format!(
243-
"Invalid COSE protected header `type` field, err: {e}"
244-
));
245-
},
246-
}
247-
},
248-
None => errors.push("Invalid COSE protected header, missing `type` field".to_string()),
249-
};
250-
251-
match cose_protected_header_find(cose, "id") {
252-
Some(doc_id) => {
253-
match decode_cbor_uuid(&doc_id) {
254-
Ok(doc_id_uuid) => {
255-
if doc_id_uuid.get_version_num() == 7 {
256-
metadata.id = doc_id_uuid;
257-
} else {
258-
errors.push(format!("Document ID is not a valid UUIDv7: {doc_id_uuid}"));
259-
}
260-
},
261-
Err(e) => {
262-
errors.push(format!(
263-
"Invalid COSE protected header `id` field, err: {e}"
264-
));
265-
},
266-
}
267-
},
268-
None => errors.push("Invalid COSE protected header, missing `id` field".to_string()),
269-
};
270-
271-
match cose_protected_header_find(cose, "ver") {
272-
Some(doc_ver) => {
273-
match decode_cbor_uuid(&doc_ver) {
274-
Ok(doc_ver_uuid) => {
275-
let mut is_valid = true;
276-
if doc_ver_uuid.get_version_num() != 7 {
277-
errors.push(format!(
278-
"Document Version is not a valid UUIDv7: {doc_ver_uuid}"
279-
));
280-
is_valid = false;
281-
}
282-
if doc_ver_uuid < metadata.id {
283-
errors.push(format!(
284-
"Document Version {doc_ver_uuid} cannot be smaller than Document ID {0}", metadata.id
285-
));
286-
is_valid = false;
287-
}
288-
if is_valid {
289-
metadata.ver = doc_ver_uuid;
290-
}
291-
},
292-
Err(e) => {
293-
errors.push(format!(
294-
"Invalid COSE protected header `ver` field, err: {e}"
295-
));
296-
},
297-
}
298-
},
299-
None => errors.push("Invalid COSE protected header, missing `ver` field".to_string()),
300-
}
301-
302-
if let Some(cbor_doc_ref) = cose_protected_header_find(cose, "ref") {
303-
match decode_cbor_document_ref(&cbor_doc_ref) {
304-
Ok(doc_ref) => {
305-
metadata.r#ref = Some(doc_ref);
306-
},
307-
Err(e) => {
308-
errors.push(format!(
309-
"Invalid COSE protected header `ref` field, err: {e}"
310-
));
311-
},
312-
}
313-
}
314-
315-
if let Some(cbor_doc_template) = cose_protected_header_find(cose, "template") {
316-
match decode_cbor_document_ref(&cbor_doc_template) {
317-
Ok(doc_template) => {
318-
metadata.template = Some(doc_template);
319-
},
320-
Err(e) => {
321-
errors.push(format!(
322-
"Invalid COSE protected header `template` field, err: {e}"
323-
));
324-
},
325-
}
326-
}
327-
328-
if let Some(cbor_doc_reply) = cose_protected_header_find(cose, "reply") {
329-
match decode_cbor_document_ref(&cbor_doc_reply) {
330-
Ok(doc_reply) => {
331-
metadata.reply = Some(doc_reply);
332-
},
333-
Err(e) => {
334-
errors.push(format!(
335-
"Invalid COSE protected header `reply` field, err: {e}"
336-
));
337-
},
338-
}
339-
}
340-
341-
if let Some(cbor_doc_section) = cose_protected_header_find(cose, "section") {
342-
match cbor_doc_section.into_text() {
343-
Ok(doc_section) => {
344-
metadata.section = Some(doc_section);
345-
},
346-
Err(e) => {
347-
errors.push(format!(
348-
"Invalid COSE protected header `section` field, err: {e:?}"
349-
));
350-
},
351-
}
352-
}
353-
354-
(metadata, ContentErrors(errors))
355-
}

0 commit comments

Comments
 (0)