Skip to content

Commit

Permalink
wip(rust/signed_doc): Impl Display for CatalystSignedDocument, add in…
Browse files Browse the repository at this point in the history
…spect example
  • Loading branch information
saibatizoku committed Dec 16, 2024
1 parent f06b177 commit 96846f0
Show file tree
Hide file tree
Showing 2 changed files with 275 additions and 8 deletions.
54 changes: 54 additions & 0 deletions rust/signed_doc/examples/cat-signed-doc.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
//! Inspect a Catalyst Signed Document.
use std::{
fs::{
// read_to_string,
File,
},
io::{
Read,
// Write
},
path::PathBuf,
};

use clap::Parser;
use signed_doc::CatalystSignedDocument;

/// Hermes cli commands
#[derive(clap::Parser)]
enum Cli {
/// Inspects COSE document
Inspect {
/// Path to the fully formed (should has at least one signature) COSE document
cose_sign: PathBuf,
/// Path to the json schema (Draft 7) to validate document against it
doc_schema: PathBuf,
},
}

impl Cli {
/// Execute Cli command
fn exec(self) -> anyhow::Result<()> {
match self {
Self::Inspect {
cose_sign,
doc_schema: _,
} => {
//
let mut cose_file = File::open(cose_sign)?;
let mut cose_file_bytes = Vec::new();
cose_file.read_to_end(&mut cose_file_bytes)?;
let cat_signed_doc: CatalystSignedDocument = cose_file_bytes.try_into()?;
println!("{cat_signed_doc}");
Ok(())
},
}
}
}

fn main() {
println!("Catalyst Signed Document");
if let Err(err) = Cli::parse().exec() {
println!("{err}");
}
}
229 changes: 221 additions & 8 deletions rust/signed_doc/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
//! Catalyst documents signing crate
use std::{convert::TryFrom, sync::Arc};
#![allow(dead_code)]
use std::{
convert::TryFrom,
fmt::{Display, Formatter},
sync::Arc,
};

use coset::CborSerializable;

/// Keep all the contents private.
/// Better even to use a structure like this. Wrapping in an Arc means we don't have to
Expand All @@ -12,12 +19,26 @@ pub struct CatalystSignedDocument {
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: {:?}", self.inner.signatures)?;
write!(f, "Content Errors: {:?}", self.content_errors)
}
}

#[derive(Default)]
/// Inner type that holds the Catalyst Signed Document with parsing errors.
struct InnerCatalystSignedDocument {
/// Document Metadata
metadata: Metadata,
/// Raw payload
_raw_doc: Vec<u8>,
/// JSON Payload
payload: serde_json::Value,
/// Signatures
signatures: Vec<coset::CoseSignature>,
/// Raw COSE Sign bytes
cose_sign: coset::CoseSign,
}

/// Document Metadata.
Expand All @@ -39,8 +60,22 @@ pub struct Metadata {
pub section: Option<String>,
}

impl Default for Metadata {
fn default() -> Self {
Self {
r#type: CatalystSignedDocument::INVALID_UUID,
id: CatalystSignedDocument::INVALID_UUID,
ver: CatalystSignedDocument::INVALID_UUID,
r#ref: None,
template: None,
reply: None,
section: None,
}
}
}

/// Reference to a Document.
#[derive(Debug, serde::Deserialize)]
#[derive(Copy, Clone, Debug, serde::Deserialize)]
#[serde(untagged)]
pub enum DocumentRef {
/// Reference to the latest document
Expand All @@ -62,17 +97,39 @@ pub enum DocumentRef {
// multiple parameters to actually create the type. This is much more elegant to use this
// way, in code.
impl TryFrom<Vec<u8>> for CatalystSignedDocument {
type Error = &'static str;
type Error = anyhow::Error;

#[allow(clippy::todo)]
fn try_from(_value: Vec<u8>) -> Result<Self, Self::Error> {
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 payload = match &cose.payload {
Some(payload) => {
let mut buf = Vec::new();
let mut bytes = payload.as_slice();
brotli::BrotliDecompress(&mut bytes, &mut buf)?;
serde_json::from_slice(&buf)?
},
None => {
println!("COSE missing payload field with the JSON content in it");
serde_json::Value::Object(serde_json::Map::new())
},
};
let inner = InnerCatalystSignedDocument {
cose_sign: cose,
payload,
..Default::default()
};
Ok(CatalystSignedDocument {
inner: Arc::new(inner),
content_errors: Vec::new(),
})
}
}

impl CatalystSignedDocument {
/// Invalid Doc Type UUID
const _INVALID_UUID: uuid::Uuid = uuid::Uuid::from_bytes([0x00; 16]);
const INVALID_UUID: uuid::Uuid = uuid::Uuid::from_bytes([0x00; 16]);

// A bunch of getters to access the contents, or reason through the document, such as.

Expand All @@ -94,3 +151,159 @@ impl CatalystSignedDocument {
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;

/// Generate the COSE protected header used by Catalyst Signed Document.
fn cose_protected_header() -> coset::Header {
coset::HeaderBuilder::new()
.algorithm(coset::iana::Algorithm::EdDSA)
.content_format(coset::iana::CoapContentFormat::Json)
.text_value(
CONTENT_ENCODING_KEY.to_string(),
CONTENT_ENCODING_VALUE.to_string().into(),
)
.build()
}

/// Decode `CBOR` encoded `UUID`.
fn decode_cbor_uuid(val: &coset::cbor::Value) -> anyhow::Result<uuid::Uuid> {
let Some((UUID_CBOR_TAG, coset::cbor::Value::Bytes(bytes))) = val.as_tag() else {
anyhow::bail!("Invalid CBOR encoded UUID type");
};
let uuid = uuid::Uuid::from_bytes(
bytes
.clone()
.try_into()
.map_err(|_| anyhow::anyhow!("Invalid CBOR encoded UUID type, invalid bytes size"))?,
);
Ok(uuid)
}

/// Decode `CBOR` encoded `DocumentRef`.
#[allow(clippy::indexing_slicing)]
fn decode_cbor_document_ref(val: &coset::cbor::Value) -> anyhow::Result<DocumentRef> {
if let Ok(id) = decode_cbor_uuid(val) {
Ok(DocumentRef::Latest { id })
} else {
let Some(array) = val.as_array() else {
anyhow::bail!("Invalid CBOR encoded document `ref` type");
};
anyhow::ensure!(array.len() == 2, "Invalid CBOR encoded document `ref` type");
let id = decode_cbor_uuid(&array[0])?;
let ver = decode_cbor_uuid(&array[1])?;
Ok(DocumentRef::WithVer { id, ver })
}
}

/// Extract `Metadata` from `coset::CoseSign`.
fn validate_cose_protected_header(cose: &coset::CoseSign) -> anyhow::Result<Metadata> {
let expected_header = cose_protected_header();
anyhow::ensure!(
cose.protected.header.alg == expected_header.alg,
"Invalid COSE document protected header `algorithm` field"
);
anyhow::ensure!(
cose.protected.header.content_type == expected_header.content_type,
"Invalid COSE document protected header `content-type` field"
);
anyhow::ensure!(
cose.protected.header.rest.iter().any(|(key, value)| {
key == &coset::Label::Text(CONTENT_ENCODING_KEY.to_string())
&& value == &coset::cbor::Value::Text(CONTENT_ENCODING_VALUE.to_string())
}),
"Invalid COSE document protected header {CONTENT_ENCODING_KEY} field"
);
let mut metadata = Metadata::default();

let Some((_, value)) = cose
.protected
.header
.rest
.iter()
.find(|(key, _)| key == &coset::Label::Text("type".to_string()))
else {
anyhow::bail!("Invalid COSE protected header, missing `type` field");
};
metadata.r#type = decode_cbor_uuid(value)
.map_err(|e| anyhow::anyhow!("Invalid COSE protected header `type` field, err: {e}"))?;

let Some((_, value)) = cose
.protected
.header
.rest
.iter()
.find(|(key, _)| key == &coset::Label::Text("id".to_string()))
else {
anyhow::bail!("Invalid COSE protected header, missing `id` field");
};
decode_cbor_uuid(value)
.map_err(|e| anyhow::anyhow!("Invalid COSE protected header `id` field, err: {e}"))?;

let Some((_, value)) = cose
.protected
.header
.rest
.iter()
.find(|(key, _)| key == &coset::Label::Text("ver".to_string()))
else {
anyhow::bail!("Invalid COSE protected header, missing `ver` field");
};
decode_cbor_uuid(value)
.map_err(|e| anyhow::anyhow!("Invalid COSE protected header `ver` field, err: {e}"))?;

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((_, 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((_, 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((_, 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"
);
}

Ok(metadata)
}

0 comments on commit 96846f0

Please sign in to comment.