Skip to content

Commit

Permalink
feat(rust/signed-doc): implement TryFrom<Vec<u8>> for CatalystSignedD…
Browse files Browse the repository at this point in the history
…ocument

* updates cat-signed-doc example to display deserialized cose sign
  documents.
  • Loading branch information
saibatizoku committed Dec 17, 2024
1 parent d011f80 commit a69a90e
Show file tree
Hide file tree
Showing 2 changed files with 126 additions and 90 deletions.
1 change: 1 addition & 0 deletions rust/signed_doc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
215 changes: 125 additions & 90 deletions rust/signed_doc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>);

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

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, "]")
}
}

Expand All @@ -46,6 +55,8 @@ struct InnerCatalystSignedDocument {
signatures: Vec<coset::CoseSignature>,
/// Raw COSE Sign bytes
cose_sign: coset::CoseSign,
/// Content Errors found when parsing the Document
content_errors: Vec<String>,
}

/// Document Metadata.
Expand All @@ -67,6 +78,20 @@ pub struct Metadata {
pub section: Option<String>,
}

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 {
Expand Down Expand Up @@ -106,12 +131,11 @@ pub enum DocumentRef {
impl TryFrom<Vec<u8>> for CatalystSignedDocument {
type Error = anyhow::Error;

#[allow(clippy::todo)]
fn try_from(cose_bytes: Vec<u8>) -> Result<Self, Self::Error> {
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();
Expand All @@ -130,10 +154,10 @@ impl TryFrom<Vec<u8>> for CatalystSignedDocument {
payload,
signatures,
cose_sign: cose,
content_errors: content_errors.0,
};
Ok(CatalystSignedDocument {
inner: Arc::new(inner),
content_errors: content_errors.0,
})
}
}
Expand All @@ -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`.
Expand All @@ -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<DocumentRef>`.
#[must_use]
pub fn doc_ref(&self) -> Option<DocumentRef> {
self.inner.metadata.r#ref
}

/// Return Document Template `Option<DocumentRef>`.
#[must_use]
pub fn doc_template(&self) -> Option<DocumentRef> {
self.inner.metadata.template
}

/// Return Document Reply `Option<DocumentRef>`.
#[must_use]
pub fn doc_reply(&self) -> Option<DocumentRef> {
self.inner.metadata.reply
}
}

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

/// Find a value for a given key in the protected header.
fn cose_protected_header_find(cose: &coset::CoseSign, rest_key: &str) -> Option<ciborium::Value> {
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();

Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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 {
Expand Down Expand Up @@ -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))
}

0 comments on commit a69a90e

Please sign in to comment.