diff --git a/rust/signed_doc/Cargo.toml b/rust/signed_doc/Cargo.toml index 44b45f151..aa41c00b3 100644 --- a/rust/signed_doc/Cargo.toml +++ b/rust/signed_doc/Cargo.toml @@ -15,6 +15,7 @@ anyhow = "1.0.89" serde = { version = "1.0.210", features = ["derive"] } serde_json = "1.0" jsonschema = "0.18.0" +ciborium = "0.2.2" coset = "0.3.7" brotli = "7.0.0" ed25519-dalek = { version = "2.1.1", features = ["pem"] } diff --git a/rust/signed_doc/src/lib.rs b/rust/signed_doc/src/lib.rs index 704c567dd..1559c5349 100644 --- a/rust/signed_doc/src/lib.rs +++ b/rust/signed_doc/src/lib.rs @@ -8,6 +8,13 @@ use std::{ use coset::CborSerializable; +/// Catalyst Signed Document Content Encoding Key. +const CONTENT_ENCODING_KEY: &str = "content encoding"; +/// Catalyst Signed Document Content Encoding Value. +const CONTENT_ENCODING_VALUE: &str = "br"; +/// CBOR tag for UUID content. +const UUID_CBOR_TAG: u64 = 37; + /// Collection of Content Errors. pub struct ContentErrors(Vec); @@ -18,20 +25,22 @@ pub struct ContentErrors(Vec); pub struct CatalystSignedDocument { /// Catalyst Signed Document metadata, raw doc, with content errors. inner: Arc, - /// Content Errors found when parsing the Document - content_errors: Vec, } impl Display for CatalystSignedDocument { fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { - writeln!(f, "Metadata: {:?}", self.inner.metadata)?; - writeln!(f, "JSON Payload: {}", self.inner.payload)?; - writeln!(f, "Signatures:")?; + writeln!(f, "{}", self.inner.metadata)?; + writeln!(f, "JSON Payload {:#}", self.inner.payload)?; + writeln!(f, "Signatures [")?; for signature in &self.inner.signatures { - writeln!(f, "\t{}", hex::encode(signature.signature.as_slice()))?; + writeln!(f, " {:#}", hex::encode(signature.signature.as_slice()))?; + } + writeln!(f, "]")?; + writeln!(f, "Content Errors [")?; + for error in &self.inner.content_errors { + writeln!(f, " {error:#}")?; } - writeln!(f, "Content Errors: {:#?}", self.content_errors)?; - write!(f, "COSE Sign: {:?}", self.inner.cose_sign) + writeln!(f, "]") } } @@ -46,6 +55,8 @@ struct InnerCatalystSignedDocument { signatures: Vec, /// Raw COSE Sign bytes cose_sign: coset::CoseSign, + /// Content Errors found when parsing the Document + content_errors: Vec, } /// Document Metadata. @@ -67,6 +78,20 @@ pub struct Metadata { pub section: Option, } +impl Display for Metadata { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { + writeln!(f, "Metadata {{")?; + writeln!(f, " doc_type: {},", self.r#type)?; + writeln!(f, " doc_id: {},", self.id)?; + writeln!(f, " doc_ver: {},", self.ver)?; + writeln!(f, " doc_ref: {:?},", self.r#ref)?; + writeln!(f, " doc_template: {:?},", self.template)?; + writeln!(f, " doc_reply: {:?},", self.reply)?; + writeln!(f, " doc_section: {:?}", self.section)?; + writeln!(f, "}}") + } +} + impl Default for Metadata { fn default() -> Self { Self { @@ -106,12 +131,11 @@ pub enum DocumentRef { impl TryFrom> for CatalystSignedDocument { type Error = anyhow::Error; - #[allow(clippy::todo)] fn try_from(cose_bytes: Vec) -> Result { let cose = coset::CoseSign::from_slice(&cose_bytes) .map_err(|e| anyhow::anyhow!("Invalid COSE Sign document: {e}"))?; - let (metadata, content_errors) = metadata_from_cose_protected_header(&cose)?; + let (metadata, content_errors) = metadata_from_cose_protected_header(&cose); let payload = match &cose.payload { Some(payload) => { let mut buf = Vec::new(); @@ -130,10 +154,10 @@ impl TryFrom> for CatalystSignedDocument { payload, signatures, cose_sign: cose, + content_errors: content_errors.0, }; Ok(CatalystSignedDocument { inner: Arc::new(inner), - content_errors: content_errors.0, }) } } @@ -147,7 +171,7 @@ impl CatalystSignedDocument { /// Are there any validation errors (as opposed to structural errors. #[must_use] pub fn has_error(&self) -> bool { - !self.content_errors.is_empty() + !self.inner.content_errors.is_empty() } /// Return Document Type `UUIDv4`. @@ -161,14 +185,31 @@ impl CatalystSignedDocument { pub fn doc_id(&self) -> uuid::Uuid { self.inner.metadata.id } -} -/// Catalyst Signed Document Content Encoding Key. -const CONTENT_ENCODING_KEY: &str = "content encoding"; -/// Catalyst Signed Document Content Encoding Value. -const CONTENT_ENCODING_VALUE: &str = "br"; -/// CBOR tag for UUID content. -const UUID_CBOR_TAG: u64 = 37; + /// Return Document Version `UUIDv7`. + #[must_use] + pub fn doc_ver(&self) -> uuid::Uuid { + self.inner.metadata.ver + } + + /// Return Last Document Reference `Option`. + #[must_use] + pub fn doc_ref(&self) -> Option { + self.inner.metadata.r#ref + } + + /// Return Document Template `Option`. + #[must_use] + pub fn doc_template(&self) -> Option { + self.inner.metadata.template + } + + /// Return Document Reply `Option`. + #[must_use] + pub fn doc_reply(&self) -> Option { + self.inner.metadata.reply + } +} /// Generate the COSE protected header used by Catalyst Signed Document. fn cose_protected_header() -> coset::Header { @@ -212,11 +253,19 @@ fn decode_cbor_document_ref(val: &coset::cbor::Value) -> anyhow::Result Option { + cose.protected + .header + .rest + .iter() + .find(|(key, _)| key == &coset::Label::Text(rest_key.to_string())) + .map(|(_, value)| value.clone()) +} + /// Extract `Metadata` from `coset::CoseSign`. #[allow(clippy::too_many_lines)] -fn metadata_from_cose_protected_header( - cose: &coset::CoseSign, -) -> anyhow::Result<(Metadata, ContentErrors)> { +fn metadata_from_cose_protected_header(cose: &coset::CoseSign) -> (Metadata, ContentErrors) { let expected_header = cose_protected_header(); let mut errors = Vec::new(); @@ -238,15 +287,9 @@ fn metadata_from_cose_protected_header( } let mut metadata = Metadata::default(); - match cose - .protected - .header - .rest - .iter() - .find(|(key, _)| key == &coset::Label::Text("type".to_string())) - { - Some((_, doc_type)) => { - match decode_cbor_uuid(doc_type) { + match cose_protected_header_find(cose, "type") { + Some(doc_type) => { + match decode_cbor_uuid(&doc_type) { Ok(doc_type_uuid) => { if doc_type_uuid.get_version_num() == 4 { metadata.r#type = doc_type_uuid; @@ -266,15 +309,9 @@ fn metadata_from_cose_protected_header( None => errors.push("Invalid COSE protected header, missing `type` field".to_string()), }; - match cose - .protected - .header - .rest - .iter() - .find(|(key, _)| key == &coset::Label::Text("id".to_string())) - { - Some((_, doc_id)) => { - match decode_cbor_uuid(doc_id) { + match cose_protected_header_find(cose, "id") { + Some(doc_id) => { + match decode_cbor_uuid(&doc_id) { Ok(doc_id_uuid) => { if doc_id_uuid.get_version_num() == 7 { metadata.id = doc_id_uuid; @@ -292,15 +329,9 @@ fn metadata_from_cose_protected_header( None => errors.push("Invalid COSE protected header, missing `id` field".to_string()), }; - match cose - .protected - .header - .rest - .iter() - .find(|(key, _)| key == &coset::Label::Text("ver".to_string())) - { - Some((_, doc_ver)) => { - match decode_cbor_uuid(doc_ver) { + match cose_protected_header_find(cose, "ver") { + Some(doc_ver) => { + match decode_cbor_uuid(&doc_ver) { Ok(doc_ver_uuid) => { let mut is_valid = true; if doc_ver_uuid.get_version_num() != 7 { @@ -329,53 +360,57 @@ fn metadata_from_cose_protected_header( None => errors.push("Invalid COSE protected header, missing `ver` field".to_string()), } - if let Some((_, value)) = cose - .protected - .header - .rest - .iter() - .find(|(key, _)| key == &coset::Label::Text("ref".to_string())) - { - decode_cbor_document_ref(value) - .map_err(|e| anyhow::anyhow!("Invalid COSE protected header `ref` field, err: {e}"))?; + if let Some(cbor_doc_ref) = cose_protected_header_find(cose, "ref") { + match decode_cbor_document_ref(&cbor_doc_ref) { + Ok(doc_ref) => { + metadata.r#ref = Some(doc_ref); + }, + Err(e) => { + errors.push(format!( + "Invalid COSE protected header `ref` field, err: {e}" + )); + }, + } } - if let Some((_, value)) = cose - .protected - .header - .rest - .iter() - .find(|(key, _)| key == &coset::Label::Text("template".to_string())) - { - decode_cbor_document_ref(value).map_err(|e| { - anyhow::anyhow!("Invalid COSE protected header `template` field, err: {e}") - })?; + if let Some(cbor_doc_template) = cose_protected_header_find(cose, "template") { + match decode_cbor_document_ref(&cbor_doc_template) { + Ok(doc_template) => { + metadata.template = Some(doc_template); + }, + Err(e) => { + errors.push(format!( + "Invalid COSE protected header `template` field, err: {e}" + )); + }, + } } - if let Some((_, value)) = cose - .protected - .header - .rest - .iter() - .find(|(key, _)| key == &coset::Label::Text("reply".to_string())) - { - decode_cbor_document_ref(value).map_err(|e| { - anyhow::anyhow!("Invalid COSE protected header `reply` field, err: {e}") - })?; + if let Some(cbor_doc_reply) = cose_protected_header_find(cose, "reply") { + match decode_cbor_document_ref(&cbor_doc_reply) { + Ok(doc_reply) => { + metadata.reply = Some(doc_reply); + }, + Err(e) => { + errors.push(format!( + "Invalid COSE protected header `reply` field, err: {e}" + )); + }, + } } - if let Some((_, value)) = cose - .protected - .header - .rest - .iter() - .find(|(key, _)| key == &coset::Label::Text("section".to_string())) - { - anyhow::ensure!( - value.is_text(), - "Invalid COSE protected header, missing `section` field" - ); + if let Some(cbor_doc_section) = cose_protected_header_find(cose, "section") { + match cbor_doc_section.into_text() { + Ok(doc_section) => { + metadata.section = Some(doc_section); + }, + Err(e) => { + errors.push(format!( + "Invalid COSE protected header `section` field, err: {e:?}" + )); + }, + } } - Ok((metadata, ContentErrors(errors))) + (metadata, ContentErrors(errors)) }