From a92c08976b569afa54300810af97877940897208 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Rosales?= Date: Tue, 10 Dec 2024 08:34:55 -0600 Subject: [PATCH 01/71] fix(rust/signed_doc): replace ULID with UUIDv7 --- rust/signed_doc/Cargo.toml | 3 +- rust/signed_doc/README.md | 17 ++++---- rust/signed_doc/examples/mk_signed_doc.rs | 47 +++++++---------------- 3 files changed, 22 insertions(+), 45 deletions(-) diff --git a/rust/signed_doc/Cargo.toml b/rust/signed_doc/Cargo.toml index 4557a8b3e46..b1c2b843099 100644 --- a/rust/signed_doc/Cargo.toml +++ b/rust/signed_doc/Cargo.toml @@ -21,5 +21,4 @@ jsonschema = "0.18.0" coset = "0.3.7" brotli = "7.0.0" ed25519-dalek = { version = "2.1.1", features = ["pem"] } -uuid = { version = "1.10.0", features = ["v4", "serde"] } -ulid = { version = "1.1.3", features = ["serde"] } \ No newline at end of file +uuid = { version = "1.10.0", features = ["v4", "v7", "serde"] } diff --git a/rust/signed_doc/README.md b/rust/signed_doc/README.md index 78a365ff9e0..314b2838ebb 100644 --- a/rust/signed_doc/README.md +++ b/rust/signed_doc/README.md @@ -27,11 +27,11 @@ which **must** be present (most of the fields originally defined by this (this parameter is used to indicate the content encodings algorithm of the payload data, in this particular case [brotli] compression data format is used). * `type`: CBOR encoded UUID. -* `id`: CBOR encoded ULID. -* `ver`: CBOR encoded ULID. -* `ref`: CBOR encoded ULID or two elements array of ULIDs (optional). -* `template`: CBOR encoded ULID or two elements array of ULIDs (optional). -* `reply`: CBOR encoded ULID or two elements array of ULIDs (optional). +* `id`: CBOR encoded UUIDv7. +* `ver`: CBOR encoded UUIDv7. +* `ref`: CBOR encoded UUIDv7 or two elements array of UUIDv7 (optional). +* `template`: CBOR encoded UUIDv7 or two elements array of UUIDv7 (optional). +* `reply`: CBOR encoded UUIDv7 or two elements array of UUIDv7 (optional). * `section`: CBOR encoded string (optional). * `collabs`: CBOR encoded array of any CBOR types (optional). @@ -46,8 +46,8 @@ protected_header = { 3 => 30, ; "content type": Json "content encoding" => "br", ; payload content encoding, brotli compression "type" => UUID, - "id" => ULID, - "ver" => ULID, + "id" => UUIDv7, + "ver" => UUIDv7, ? "ref" => reference_type, ? "template" => reference_type, ? "reply" => reference_type, @@ -56,8 +56,7 @@ protected_header = { } UUID = #6.37(bytes) -ULID = #6.32780(bytes) -reference_type = ULID / [ULID, ULID] ; either ULID or [ULID, ULID] +reference_type = UUIDv7 / [UUIDv7, UUIDv7] ; either UUIDv7 or [UUIDv7, UUIDv7] ``` ### COSE payload diff --git a/rust/signed_doc/examples/mk_signed_doc.rs b/rust/signed_doc/examples/mk_signed_doc.rs index 0eb8c15c06f..1ed47ed04b6 100644 --- a/rust/signed_doc/examples/mk_signed_doc.rs +++ b/rust/signed_doc/examples/mk_signed_doc.rs @@ -59,13 +59,12 @@ enum Cli { const CONTENT_ENCODING_KEY: &str = "content encoding"; const CONTENT_ENCODING_VALUE: &str = "br"; const UUID_CBOR_TAG: u64 = 37; -const ULID_CBOR_TAG: u64 = 32780; #[derive(Debug, serde::Deserialize)] struct Metadata { r#type: uuid::Uuid, - id: ulid::Ulid, - ver: ulid::Ulid, + id: uuid::Uuid, + ver: uuid::Uuid, r#ref: Option, template: Option, reply: Option, @@ -76,29 +75,9 @@ struct Metadata { #[serde(untagged)] enum DocumentRef { /// Reference to the latest document - Latest { id: ulid::Ulid }, + Latest { id: uuid::Uuid }, /// Reference to the specific document version - WithVer { id: ulid::Ulid, ver: ulid::Ulid }, -} - -fn encode_cbor_ulid(ulid: &ulid::Ulid) -> coset::cbor::Value { - coset::cbor::Value::Tag( - ULID_CBOR_TAG, - coset::cbor::Value::Bytes(ulid.to_bytes().to_vec()).into(), - ) -} - -fn decode_cbor_ulid(val: &coset::cbor::Value) -> anyhow::Result { - let Some((ULID_CBOR_TAG, coset::cbor::Value::Bytes(bytes))) = val.as_tag() else { - anyhow::bail!("Invalid CBOR encoded ULID type"); - }; - let ulid = ulid::Ulid::from_bytes( - bytes - .clone() - .try_into() - .map_err(|_| anyhow::anyhow!("Invalid CBOR encoded ULID type, invalid bytes size"))?, - ); - Ok(ulid) + WithVer { id: uuid::Uuid, ver: uuid::Uuid }, } fn encode_cbor_uuid(uuid: &uuid::Uuid) -> coset::cbor::Value { @@ -123,24 +102,24 @@ fn decode_cbor_uuid(val: &coset::cbor::Value) -> anyhow::Result { fn encode_cbor_document_ref(doc_ref: &DocumentRef) -> coset::cbor::Value { match doc_ref { - DocumentRef::Latest { id } => encode_cbor_ulid(id), + DocumentRef::Latest { id } => encode_cbor_uuid(id), DocumentRef::WithVer { id, ver } => { - coset::cbor::Value::Array(vec![encode_cbor_ulid(id), encode_cbor_ulid(ver)]) + coset::cbor::Value::Array(vec![encode_cbor_uuid(id), encode_cbor_uuid(ver)]) }, } } #[allow(clippy::indexing_slicing)] fn decode_cbor_document_ref(val: &coset::cbor::Value) -> anyhow::Result { - if let Ok(id) = decode_cbor_ulid(val) { + 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_ulid(&array[0])?; - let ver = decode_cbor_ulid(&array[1])?; + let id = decode_cbor_uuid(&array[0])?; + let ver = decode_cbor_uuid(&array[1])?; Ok(DocumentRef::WithVer { id, ver }) } } @@ -243,11 +222,11 @@ fn build_empty_cose_doc(doc_bytes: Vec, meta: &Metadata) -> coset::CoseSign )); protected_header.rest.push(( coset::Label::Text("id".to_string()), - encode_cbor_ulid(&meta.id), + encode_cbor_uuid(&meta.id), )); protected_header.rest.push(( coset::Label::Text("ver".to_string()), - encode_cbor_ulid(&meta.ver), + encode_cbor_uuid(&meta.ver), )); if let Some(r#ref) = &meta.r#ref { protected_header.rest.push(( @@ -388,7 +367,7 @@ fn validate_cose_protected_header(cose: &coset::CoseSign) -> anyhow::Result<()> else { anyhow::bail!("Invalid COSE protected header, missing `id` field"); }; - decode_cbor_ulid(value) + decode_cbor_uuid(value) .map_err(|e| anyhow::anyhow!("Invalid COSE protected header `id` field, err: {e}"))?; let Some((_, value)) = cose @@ -400,7 +379,7 @@ fn validate_cose_protected_header(cose: &coset::CoseSign) -> anyhow::Result<()> else { anyhow::bail!("Invalid COSE protected header, missing `ver` field"); }; - decode_cbor_ulid(value) + decode_cbor_uuid(value) .map_err(|e| anyhow::anyhow!("Invalid COSE protected header `ver` field, err: {e}"))?; if let Some((_, value)) = cose From 8ee9c02810bb04d61562518da08aa22b212bcaca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Rosales?= Date: Tue, 10 Dec 2024 17:35:03 -0600 Subject: [PATCH 02/71] fix(rust/signed_doc): update meta.schema.json with definitions for uuid v4 and v7 --- rust/signed_doc/meta.schema.json | 57 +++++++++++++++----------------- 1 file changed, 26 insertions(+), 31 deletions(-) diff --git a/rust/signed_doc/meta.schema.json b/rust/signed_doc/meta.schema.json index 88cfec84afe..fad6bf93d9d 100644 --- a/rust/signed_doc/meta.schema.json +++ b/rust/signed_doc/meta.schema.json @@ -2,29 +2,33 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "description": "Catalyst signed document metadata json schema", - "type": "object", - "additionalProperties": false, - "properties": { - "type": { + "definitions": { + "uuidv4": { "type": "string", "format": "uuid", "examples": [ "0ce8ab38-9258-4fbc-a62e-7faa6e58318f" ] }, - "id": { + "uuidv7": { "type": "string", - "format": "ulid", + "format": "uuid", "examples": [ - "01JE99R792FWCQFZPHJH1R87RB" + "0193ae7c-8131-7fe6-91f0-b451ea229b11" ] + } + }, + "type": "object", + "additionalProperties": false, + "properties": { + "type": { + "$ref": "#/definitions/uuidv4" + }, + "id": { + "$ref": "#/definitions/uuidv7" }, "ver": { - "type": "string", - "format": "ulid", - "examples": [ - "01JE99R792FWCQFZPHJH1R87RB" - ] + "$ref": "#/definitions/uuidv7" }, "ref": { "anyOf": [ @@ -32,8 +36,7 @@ "type": "object", "properties": { "id": { - "type": "string", - "format": "ulid" + "$ref": "#/definitions/uuidv7" } } }, @@ -41,12 +44,10 @@ "type": "object", "properties": { "id": { - "type": "string", - "format": "ulid" + "$ref": "#/definitions/uuidv7" }, "ver": { - "type": "string", - "format": "ulid" + "$ref": "#/definitions/uuidv7" } } } @@ -58,8 +59,7 @@ "type": "object", "properties": { "id": { - "type": "string", - "format": "ulid" + "$ref": "#/definitions/uuidv7" } } }, @@ -67,12 +67,10 @@ "type": "object", "properties": { "id": { - "type": "string", - "format": "ulid" + "$ref": "#/definitions/uuidv7" }, "ver": { - "type": "string", - "format": "ulid" + "$ref": "#/definitions/uuidv7" } } } @@ -84,8 +82,7 @@ "type": "object", "properties": { "id": { - "type": "string", - "format": "ulid" + "$ref": "#/definitions/uuidv7" } } }, @@ -93,12 +90,10 @@ "type": "object", "properties": { "id": { - "type": "string", - "format": "ulid" + "$ref": "#/definitions/uuidv7" }, "ver": { - "type": "string", - "format": "ulid" + "$ref": "#/definitions/uuidv7" } } } @@ -113,4 +108,4 @@ "id", "ver" ] -} \ No newline at end of file +} From de6cb2c5ea15b9244282bdc4304eeb2240b1af38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Rosales?= Date: Tue, 10 Dec 2024 23:15:42 -0600 Subject: [PATCH 03/71] wip(rust/signed_doc): add basic structure for catalyst signed document API * refactor code from mk_signed_doc example into src/lib.rs --- .config/dictionaries/project.dic | 2 + rust/signed_doc/Cargo.toml | 6 +- rust/signed_doc/examples/mk_signed_doc.rs | 21 +----- rust/signed_doc/src/lib.rs | 88 +++++++++++++++++++++++ 4 files changed, 94 insertions(+), 23 deletions(-) diff --git a/.config/dictionaries/project.dic b/.config/dictionaries/project.dic index c6b58560206..c559b8775b5 100644 --- a/.config/dictionaries/project.dic +++ b/.config/dictionaries/project.dic @@ -260,6 +260,8 @@ upnp ureq userid utimensat +uuidv4 +uuidv7 UTXO vitss Vkey diff --git a/rust/signed_doc/Cargo.toml b/rust/signed_doc/Cargo.toml index b1c2b843099..db525033eb8 100644 --- a/rust/signed_doc/Cargo.toml +++ b/rust/signed_doc/Cargo.toml @@ -11,9 +11,6 @@ license.workspace = true workspace = true [dependencies] - -[dev-dependencies] -clap = { version = "4.5.19", features = ["derive", "env"] } anyhow = "1.0.89" serde = { version = "1.0.210", features = ["derive"] } serde_json = "1.0" @@ -22,3 +19,6 @@ coset = "0.3.7" brotli = "7.0.0" ed25519-dalek = { version = "2.1.1", features = ["pem"] } uuid = { version = "1.10.0", features = ["v4", "v7", "serde"] } + +[dev-dependencies] +clap = { version = "4.5.19", features = ["derive", "env"] } diff --git a/rust/signed_doc/examples/mk_signed_doc.rs b/rust/signed_doc/examples/mk_signed_doc.rs index 1ed47ed04b6..c891fac3e5f 100644 --- a/rust/signed_doc/examples/mk_signed_doc.rs +++ b/rust/signed_doc/examples/mk_signed_doc.rs @@ -14,6 +14,7 @@ use ed25519_dalek::{ ed25519::signature::Signer, pkcs8::{DecodePrivateKey, DecodePublicKey}, }; +use signed_doc::{DocumentRef, Metadata}; fn main() { if let Err(err) = Cli::parse().exec() { @@ -60,26 +61,6 @@ const CONTENT_ENCODING_KEY: &str = "content encoding"; const CONTENT_ENCODING_VALUE: &str = "br"; const UUID_CBOR_TAG: u64 = 37; -#[derive(Debug, serde::Deserialize)] -struct Metadata { - r#type: uuid::Uuid, - id: uuid::Uuid, - ver: uuid::Uuid, - r#ref: Option, - template: Option, - reply: Option, - section: Option, -} - -#[derive(Debug, serde::Deserialize)] -#[serde(untagged)] -enum DocumentRef { - /// Reference to the latest document - Latest { id: uuid::Uuid }, - /// Reference to the specific document version - WithVer { id: uuid::Uuid, ver: uuid::Uuid }, -} - fn encode_cbor_uuid(uuid: &uuid::Uuid) -> coset::cbor::Value { coset::cbor::Value::Tag( UUID_CBOR_TAG, diff --git a/rust/signed_doc/src/lib.rs b/rust/signed_doc/src/lib.rs index 8ed8e786869..34330464e15 100644 --- a/rust/signed_doc/src/lib.rs +++ b/rust/signed_doc/src/lib.rs @@ -1 +1,89 @@ //! Catalyst documents signing crate +use std::{convert::TryFrom, sync::Arc}; + +/// Keep all the contents private. +/// Better even to use a structure like this. Wrapping in an Arc means we don't have to +/// manage the Arc anywhere else. These are likely to be large, best to have the Arc be +/// non-optional. +pub struct CatalystSignedDocument { + /// Catalyst Signed Document metadata, raw doc, with content errors. + inner: Arc, + /// Content Errors found when parsing the Document + content_errors: Vec, +} + +/// Inner type that holds the Catalyst Signed Document with parsing errors. +struct InnerCatalystSignedDocument { + /// Document Metadata + _metadata: Metadata, + /// Raw payload + _raw_doc: Vec, +} + +/// Document Metadata. +#[derive(Debug, serde::Deserialize)] +pub struct Metadata { + /// Document Type `UUIDv7`. + pub r#type: uuid::Uuid, + /// Document ID `UUIDv7`. + pub id: uuid::Uuid, + /// Document Version `UUIDv7`. + pub ver: uuid::Uuid, + /// Reference to the latest document. + pub r#ref: Option, + /// Reference to the document template. + pub template: Option, + /// Reference to the document reply. + pub reply: Option, + /// Reference to the document section. + pub section: Option, +} + +/// Reference to a Document. +#[derive(Debug, serde::Deserialize)] +#[serde(untagged)] +pub enum DocumentRef { + /// Reference to the latest document + Latest { + /// Document ID UUID + id: uuid::Uuid, + }, + /// Reference to the specific document version + WithVer { + /// Document ID UUID + id: uuid::Uuid, + /// Document Version UUID + ver: uuid::Uuid, + }, +} + +// Do this instead of `new` if we are converting a single parameter into a struct/type we +// should use either `From` or `TryFrom` and reserve `new` for cases where we need +// multiple parameters to actually create the type. This is much more elegant to use this +// way, in code. +impl TryFrom> for CatalystSignedDocument { + type Error = &'static str; + + fn try_from(value: Vec) -> Result { + todo!(); + } +} + +impl CatalystSignedDocument { + /// Invalid Doc Type UUID + 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. + + /// Are there any validation errors (as opposed to structural errors. + #[must_use] + pub fn has_error(&self) -> bool { + !self.content_errors.is_empty() + } + + /// Return Document Type UUID. + #[must_use] + pub fn doc_type(&self) -> uuid::Uuid { + INVALID_UUID + } // Can compare it against INVALID_DOC_TYPE to see if its valid or not. +} From e12da327f51cda12a40c0144e8e2839482fd6d57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Rosales?= Date: Tue, 10 Dec 2024 23:47:13 -0600 Subject: [PATCH 04/71] wip(rust/signed_doc): add API methods to get document metadata uuids --- rust/signed_doc/src/lib.rs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/rust/signed_doc/src/lib.rs b/rust/signed_doc/src/lib.rs index 34330464e15..469fcbce406 100644 --- a/rust/signed_doc/src/lib.rs +++ b/rust/signed_doc/src/lib.rs @@ -15,7 +15,7 @@ pub struct CatalystSignedDocument { /// Inner type that holds the Catalyst Signed Document with parsing errors. struct InnerCatalystSignedDocument { /// Document Metadata - _metadata: Metadata, + metadata: Metadata, /// Raw payload _raw_doc: Vec, } @@ -64,14 +64,15 @@ pub enum DocumentRef { impl TryFrom> for CatalystSignedDocument { type Error = &'static str; - fn try_from(value: Vec) -> Result { + #[allow(clippy::todo)] + fn try_from(_value: Vec) -> Result { todo!(); } } 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. @@ -81,9 +82,15 @@ impl CatalystSignedDocument { !self.content_errors.is_empty() } - /// Return Document Type UUID. + /// Return Document Type `UUIDv4`. #[must_use] pub fn doc_type(&self) -> uuid::Uuid { - INVALID_UUID - } // Can compare it against INVALID_DOC_TYPE to see if its valid or not. + self.inner.metadata.r#type + } + + /// Return Document ID `UUIDv7`. + #[must_use] + pub fn doc_id(&self) -> uuid::Uuid { + self.inner.metadata.id + } } From 9327287bca7355db7e86d08ebf606c1bdc2438eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Rosales?= Date: Wed, 11 Dec 2024 12:16:39 -0600 Subject: [PATCH 05/71] fix(rust/signed_doc): fix README --- rust/signed_doc/README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/rust/signed_doc/README.md b/rust/signed_doc/README.md index 314b2838ebb..fc9a80402d9 100644 --- a/rust/signed_doc/README.md +++ b/rust/signed_doc/README.md @@ -27,11 +27,11 @@ which **must** be present (most of the fields originally defined by this (this parameter is used to indicate the content encodings algorithm of the payload data, in this particular case [brotli] compression data format is used). * `type`: CBOR encoded UUID. -* `id`: CBOR encoded UUIDv7. -* `ver`: CBOR encoded UUIDv7. -* `ref`: CBOR encoded UUIDv7 or two elements array of UUIDv7 (optional). -* `template`: CBOR encoded UUIDv7 or two elements array of UUIDv7 (optional). -* `reply`: CBOR encoded UUIDv7 or two elements array of UUIDv7 (optional). +* `id`: CBOR encoded UUID. +* `ver`: CBOR encoded UUID. +* `ref`: CBOR encoded UUID or two elements array of UUID (optional). +* `template`: CBOR encoded UUID or two elements array of UUID (optional). +* `reply`: CBOR encoded UUID or two elements array of UUID (optional). * `section`: CBOR encoded string (optional). * `collabs`: CBOR encoded array of any CBOR types (optional). @@ -46,8 +46,8 @@ protected_header = { 3 => 30, ; "content type": Json "content encoding" => "br", ; payload content encoding, brotli compression "type" => UUID, - "id" => UUIDv7, - "ver" => UUIDv7, + "id" => UUID, + "ver" => UUID, ? "ref" => reference_type, ? "template" => reference_type, ? "reply" => reference_type, @@ -56,7 +56,7 @@ protected_header = { } UUID = #6.37(bytes) -reference_type = UUIDv7 / [UUIDv7, UUIDv7] ; either UUIDv7 or [UUIDv7, UUIDv7] +reference_type = UUID / [UUID, UUID] ; either UUID or [UUID, UUID] ``` ### COSE payload From 56ce55376ca52b01c229c05c9b982afca71c95f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Rosales?= Date: Sun, 15 Dec 2024 21:02:59 -0600 Subject: [PATCH 06/71] wip(rust/signed_doc): Impl Display for CatalystSignedDocument, add inspect example --- rust/signed_doc/examples/cat-signed-doc.rs | 54 +++++ rust/signed_doc/src/lib.rs | 229 ++++++++++++++++++++- 2 files changed, 275 insertions(+), 8 deletions(-) create mode 100644 rust/signed_doc/examples/cat-signed-doc.rs diff --git a/rust/signed_doc/examples/cat-signed-doc.rs b/rust/signed_doc/examples/cat-signed-doc.rs new file mode 100644 index 00000000000..dce556657e1 --- /dev/null +++ b/rust/signed_doc/examples/cat-signed-doc.rs @@ -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}"); + } +} diff --git a/rust/signed_doc/src/lib.rs b/rust/signed_doc/src/lib.rs index 469fcbce406..72b943eb3f6 100644 --- a/rust/signed_doc/src/lib.rs +++ b/rust/signed_doc/src/lib.rs @@ -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 @@ -12,12 +19,26 @@ pub struct CatalystSignedDocument { 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: {:?}", 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, + /// JSON Payload + payload: serde_json::Value, + /// Signatures + signatures: Vec, + /// Raw COSE Sign bytes + cose_sign: coset::CoseSign, } /// Document Metadata. @@ -39,8 +60,22 @@ pub struct Metadata { pub section: Option, } +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 @@ -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> for CatalystSignedDocument { - type Error = &'static str; + type Error = anyhow::Error; #[allow(clippy::todo)] - fn try_from(_value: Vec) -> Result { - 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 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. @@ -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 { + 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 { + 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 { + 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) +} From 9014d3653ad1d23d6ee82196e85478d210129292 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Rosales?= Date: Mon, 16 Dec 2024 00:50:43 -0600 Subject: [PATCH 07/71] wip(rust/signed_doc): print cose sign example --- rust/signed_doc/src/lib.rs | 148 +++++++++++++++++++++++++++---------- 1 file changed, 110 insertions(+), 38 deletions(-) diff --git a/rust/signed_doc/src/lib.rs b/rust/signed_doc/src/lib.rs index 72b943eb3f6..704c567dd8b 100644 --- a/rust/signed_doc/src/lib.rs +++ b/rust/signed_doc/src/lib.rs @@ -8,6 +8,9 @@ use std::{ use coset::CborSerializable; +/// Collection of Content Errors. +pub struct ContentErrors(Vec); + /// Keep all the contents private. /// Better even to use a structure like this. Wrapping in an Arc means we don't have to /// manage the Arc anywhere else. These are likely to be large, best to have the Arc be @@ -23,8 +26,12 @@ 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) + writeln!(f, "Signatures:")?; + for signature in &self.inner.signatures { + writeln!(f, "\t{}", hex::encode(signature.signature.as_slice()))?; + } + writeln!(f, "Content Errors: {:#?}", self.content_errors)?; + write!(f, "COSE Sign: {:?}", self.inner.cose_sign) } } @@ -103,6 +110,8 @@ impl TryFrom> for CatalystSignedDocument { 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 payload = match &cose.payload { Some(payload) => { let mut buf = Vec::new(); @@ -115,14 +124,16 @@ impl TryFrom> for CatalystSignedDocument { serde_json::Value::Object(serde_json::Map::new()) }, }; + let signatures = cose.signatures.clone(); let inner = InnerCatalystSignedDocument { - cose_sign: cose, + metadata, payload, - ..Default::default() + signatures, + cose_sign: cose, }; Ok(CatalystSignedDocument { inner: Arc::new(inner), - content_errors: Vec::new(), + content_errors: content_errors.0, }) } } @@ -202,60 +213,121 @@ fn decode_cbor_document_ref(val: &coset::cbor::Value) -> anyhow::Result anyhow::Result { +#[allow(clippy::too_many_lines)] +fn metadata_from_cose_protected_header( + cose: &coset::CoseSign, +) -> anyhow::Result<(Metadata, ContentErrors)> { 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 errors = Vec::new(); + + if cose.protected.header.alg != expected_header.alg { + errors.push("Invalid COSE document protected header `algorithm` field".to_string()); + } + + if cose.protected.header.content_type != expected_header.content_type { + errors.push("Invalid COSE document protected header `content-type` field".to_string()); + } + + if !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()) + }) { + errors.push( + "Invalid COSE document protected header {CONTENT_ENCODING_KEY} field".to_string(), + ); + } let mut metadata = Metadata::default(); - let Some((_, value)) = cose + match cose .protected .header .rest .iter() .find(|(key, _)| key == &coset::Label::Text("type".to_string())) - else { - anyhow::bail!("Invalid COSE protected header, missing `type` field"); + { + 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; + } else { + errors.push(format!( + "Document type is not a valid UUIDv4: {doc_type_uuid}" + )); + } + }, + Err(e) => { + errors.push(format!( + "Invalid COSE protected header `type` field, err: {e}" + )); + }, + } + }, + None => errors.push("Invalid COSE protected header, missing `type` field".to_string()), }; - metadata.r#type = decode_cbor_uuid(value) - .map_err(|e| anyhow::anyhow!("Invalid COSE protected header `type` field, err: {e}"))?; - let Some((_, value)) = cose + match cose .protected .header .rest .iter() .find(|(key, _)| key == &coset::Label::Text("id".to_string())) - else { - anyhow::bail!("Invalid COSE protected header, missing `id` field"); + { + 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; + } else { + errors.push(format!("Document ID is not a valid UUIDv7: {doc_id_uuid}")); + } + }, + Err(e) => { + errors.push(format!( + "Invalid COSE protected header `id` field, err: {e}" + )); + }, + } + }, + None => errors.push("Invalid COSE protected header, missing `id` field".to_string()), }; - decode_cbor_uuid(value) - .map_err(|e| anyhow::anyhow!("Invalid COSE protected header `id` field, err: {e}"))?; - let Some((_, value)) = cose + match 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}"))?; + { + 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 { + errors.push(format!( + "Document Version is not a valid UUIDv7: {doc_ver_uuid}" + )); + is_valid = false; + } + if doc_ver_uuid < metadata.id { + errors.push(format!( + "Document Version {doc_ver_uuid} cannot be smaller than Document ID {0}", metadata.id + )); + is_valid = false; + } + if is_valid { + metadata.ver = doc_ver_uuid; + } + }, + Err(e) => { + errors.push(format!( + "Invalid COSE protected header `ver` field, err: {e}" + )); + }, + } + }, + None => errors.push("Invalid COSE protected header, missing `ver` field".to_string()), + } if let Some((_, value)) = cose .protected @@ -305,5 +377,5 @@ fn validate_cose_protected_header(cose: &coset::CoseSign) -> anyhow::Result Date: Mon, 16 Dec 2024 00:51:43 -0600 Subject: [PATCH 08/71] feat(rust/signed-doc): add hex crate --- rust/signed_doc/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/rust/signed_doc/Cargo.toml b/rust/signed_doc/Cargo.toml index db525033eb8..44b45f15110 100644 --- a/rust/signed_doc/Cargo.toml +++ b/rust/signed_doc/Cargo.toml @@ -19,6 +19,7 @@ coset = "0.3.7" brotli = "7.0.0" ed25519-dalek = { version = "2.1.1", features = ["pem"] } uuid = { version = "1.10.0", features = ["v4", "v7", "serde"] } +hex = "0.4.3" [dev-dependencies] clap = { version = "4.5.19", features = ["derive", "env"] } From e1e86e77b98447b2b40ef73f69cf2cdbbf7bdcb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Rosales?= Date: Mon, 16 Dec 2024 20:52:37 -0600 Subject: [PATCH 09/71] feat(rust/signed-doc): implement TryFrom> for CatalystSignedDocument * updates cat-signed-doc example to display deserialized cose sign documents. --- rust/signed_doc/Cargo.toml | 1 + rust/signed_doc/src/lib.rs | 215 +++++++++++++++++++++---------------- 2 files changed, 126 insertions(+), 90 deletions(-) diff --git a/rust/signed_doc/Cargo.toml b/rust/signed_doc/Cargo.toml index 44b45f15110..aa41c00b364 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 704c567dd8b..1559c53497a 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)) } From 33ab4740e0a7102dcba08683f7430a9f7e4d57ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Rosales?= Date: Mon, 16 Dec 2024 22:27:55 -0600 Subject: [PATCH 10/71] fix(rust/signed_doc): cleanup and fix to DocumentRef serde --- rust/signed_doc/examples/cat-signed-doc.rs | 8 ++------ rust/signed_doc/examples/mk_signed_doc.rs | 5 +++-- rust/signed_doc/meta.schema.json | 14 +++++--------- rust/signed_doc/src/lib.rs | 16 ++++++---------- 4 files changed, 16 insertions(+), 27 deletions(-) diff --git a/rust/signed_doc/examples/cat-signed-doc.rs b/rust/signed_doc/examples/cat-signed-doc.rs index dce556657e1..60afc770e79 100644 --- a/rust/signed_doc/examples/cat-signed-doc.rs +++ b/rust/signed_doc/examples/cat-signed-doc.rs @@ -21,8 +21,6 @@ enum Cli { 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, }, } @@ -30,10 +28,7 @@ impl Cli { /// Execute Cli command fn exec(self) -> anyhow::Result<()> { match self { - Self::Inspect { - cose_sign, - doc_schema: _, - } => { + Self::Inspect { cose_sign } => { // let mut cose_file = File::open(cose_sign)?; let mut cose_file_bytes = Vec::new(); @@ -48,6 +43,7 @@ impl Cli { fn main() { println!("Catalyst Signed Document"); + println!("------------------------"); if let Err(err) = Cli::parse().exec() { println!("{err}"); } diff --git a/rust/signed_doc/examples/mk_signed_doc.rs b/rust/signed_doc/examples/mk_signed_doc.rs index c891fac3e5f..a78a3206e91 100644 --- a/rust/signed_doc/examples/mk_signed_doc.rs +++ b/rust/signed_doc/examples/mk_signed_doc.rs @@ -84,7 +84,7 @@ fn decode_cbor_uuid(val: &coset::cbor::Value) -> anyhow::Result { fn encode_cbor_document_ref(doc_ref: &DocumentRef) -> coset::cbor::Value { match doc_ref { DocumentRef::Latest { id } => encode_cbor_uuid(id), - DocumentRef::WithVer { id, ver } => { + DocumentRef::WithVer(id, ver) => { coset::cbor::Value::Array(vec![encode_cbor_uuid(id), encode_cbor_uuid(ver)]) }, } @@ -101,7 +101,7 @@ fn decode_cbor_document_ref(val: &coset::cbor::Value) -> anyhow::Result) -> Result<(), std::fmt::Error> { writeln!(f, "{}", self.inner.metadata)?; - writeln!(f, "JSON Payload {:#}", self.inner.payload)?; + writeln!(f, "JSON Payload {:#}\n", self.inner.payload)?; writeln!(f, "Signatures [")?; for signature in &self.inner.signatures { - writeln!(f, " {:#}", hex::encode(signature.signature.as_slice()))?; + writeln!(f, " 0x{:#}", hex::encode(signature.signature.as_slice()))?; } - writeln!(f, "]")?; + writeln!(f, "]\n")?; writeln!(f, "Content Errors [")?; for error in &self.inner.content_errors { writeln!(f, " {error:#}")?; @@ -116,12 +116,8 @@ pub enum DocumentRef { id: uuid::Uuid, }, /// Reference to the specific document version - WithVer { - /// Document ID UUID - id: uuid::Uuid, - /// Document Version UUID - ver: uuid::Uuid, - }, + /// Document ID UUID, Document Ver UUID + WithVer(uuid::Uuid, uuid::Uuid), } // Do this instead of `new` if we are converting a single parameter into a struct/type we @@ -249,7 +245,7 @@ fn decode_cbor_document_ref(val: &coset::cbor::Value) -> anyhow::Result Date: Thu, 19 Dec 2024 12:05:34 -0600 Subject: [PATCH 11/71] feat(rust/signed-doc): decode CoseSign from tagged or untagged bytes --- rust/signed_doc/src/lib.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/rust/signed_doc/src/lib.rs b/rust/signed_doc/src/lib.rs index 731744630a7..d5ec9919844 100644 --- a/rust/signed_doc/src/lib.rs +++ b/rust/signed_doc/src/lib.rs @@ -6,7 +6,7 @@ use std::{ sync::Arc, }; -use coset::CborSerializable; +use coset::{CborSerializable, TaggedCborSerializable}; /// Catalyst Signed Document Content Encoding Key. const CONTENT_ENCODING_KEY: &str = "content encoding"; @@ -128,7 +128,9 @@ impl TryFrom> for CatalystSignedDocument { type Error = anyhow::Error; fn try_from(cose_bytes: Vec) -> Result { - let cose = coset::CoseSign::from_slice(&cose_bytes) + // Try reading as a tagged COSE SIGN, otherwise try reading as untagged. + let cose = coset::CoseSign::from_tagged_slice(&cose_bytes) + .or(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); From c41bbb5d993fc9393fa7b35bfaf275be991ad50c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Rosales?= Date: Thu, 19 Dec 2024 12:53:55 -0600 Subject: [PATCH 12/71] feat(rust/signed-doc): add subcommand to inspect cose sign from hex-encoded strings --- rust/signed_doc/examples/cat-signed-doc.rs | 25 ++++++++++++++-------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/rust/signed_doc/examples/cat-signed-doc.rs b/rust/signed_doc/examples/cat-signed-doc.rs index 60afc770e79..381f72852d6 100644 --- a/rust/signed_doc/examples/cat-signed-doc.rs +++ b/rust/signed_doc/examples/cat-signed-doc.rs @@ -20,24 +20,31 @@ enum Cli { /// Inspects COSE document Inspect { /// Path to the fully formed (should has at least one signature) COSE document - cose_sign: PathBuf, + cose_sign_path: PathBuf, + }, + /// Inspect COSE document hex-formatted bytes + InspectBytes { + /// Hex-formatted COSE SIGN Bytes + cose_sign_str: String, }, } impl Cli { /// Execute Cli command fn exec(self) -> anyhow::Result<()> { - match self { - Self::Inspect { cose_sign } => { - // - let mut cose_file = File::open(cose_sign)?; + let cose_bytes = match self { + Self::Inspect { cose_sign_path } => { + let mut cose_file = File::open(cose_sign_path)?; 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(()) + cose_file_bytes }, - } + Self::InspectBytes { cose_sign_str } => hex::decode(&cose_sign_str)?, + }; + println!("Bytes read:\n{}\n", hex::encode(&cose_bytes)); + let cat_signed_doc: CatalystSignedDocument = cose_bytes.try_into()?; + println!("{cat_signed_doc}"); + Ok(()) } } From ea0bebd407300392ab6bac92cccc1c7b2f6b91cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Rosales?= Date: Thu, 19 Dec 2024 12:54:52 -0600 Subject: [PATCH 13/71] fix(rust/signed_doc): remove alg from top-level protected header --- rust/signed_doc/examples/mk_signed_doc.rs | 5 +++-- rust/signed_doc/src/lib.rs | 17 ++++++----------- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/rust/signed_doc/examples/mk_signed_doc.rs b/rust/signed_doc/examples/mk_signed_doc.rs index a78a3206e91..140f988effd 100644 --- a/rust/signed_doc/examples/mk_signed_doc.rs +++ b/rust/signed_doc/examples/mk_signed_doc.rs @@ -186,7 +186,6 @@ fn brotli_decompress_json(mut doc_bytes: &[u8]) -> anyhow::Result coset::Header { coset::HeaderBuilder::new() - .algorithm(coset::iana::Algorithm::EdDSA) .content_format(coset::iana::CoapContentFormat::Json) .text_value( CONTENT_ENCODING_KEY.to_string(), @@ -269,7 +268,9 @@ fn load_public_key_from_file(pk_path: &PathBuf) -> anyhow::Result uuid::Uuid { - self.inner.metadata.r#type + self.inner.metadata.doc_type() } /// Return Document ID `UUIDv7`. #[must_use] pub fn doc_id(&self) -> uuid::Uuid { - self.inner.metadata.id + self.inner.metadata.doc_id() } /// Return Document Version `UUIDv7`. #[must_use] pub fn doc_ver(&self) -> uuid::Uuid { - self.inner.metadata.ver + self.inner.metadata.doc_ver() } /// Return Last Document Reference `Option`. #[must_use] pub fn doc_ref(&self) -> Option { - self.inner.metadata.r#ref + self.inner.metadata.doc_ref() } /// Return Document Template `Option`. #[must_use] pub fn doc_template(&self) -> Option { - self.inner.metadata.template + self.inner.metadata.doc_template() } /// Return Document Reply `Option`. #[must_use] pub fn doc_reply(&self) -> Option { - self.inner.metadata.reply + self.inner.metadata.doc_reply() } } /// 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(), @@ -267,10 +266,6 @@ fn metadata_from_cose_protected_header(cose: &coset::CoseSign) -> (Metadata, Con let expected_header = cose_protected_header(); let mut errors = Vec::new(); - if cose.protected.header.alg != expected_header.alg { - errors.push("Invalid COSE document protected header `algorithm` field".to_string()); - } - if cose.protected.header.content_type != expected_header.content_type { errors.push("Invalid COSE document protected header `content-type` field".to_string()); } From d942e1e7e2dd9392ed5d185850923a226a9425f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Rosales?= Date: Thu, 19 Dec 2024 13:26:32 -0600 Subject: [PATCH 14/71] feat(rust/signed-doc): refactor metadata into a module --- rust/signed_doc/examples/mk_signed_doc.rs | 14 +-- rust/signed_doc/src/lib.rs | 74 ++------------- rust/signed_doc/src/metadata/document_ref.rs | 15 +++ rust/signed_doc/src/metadata/mod.rs | 99 ++++++++++++++++++++ 4 files changed, 131 insertions(+), 71 deletions(-) create mode 100644 rust/signed_doc/src/metadata/document_ref.rs create mode 100644 rust/signed_doc/src/metadata/mod.rs diff --git a/rust/signed_doc/examples/mk_signed_doc.rs b/rust/signed_doc/examples/mk_signed_doc.rs index 140f988effd..71da84cd858 100644 --- a/rust/signed_doc/examples/mk_signed_doc.rs +++ b/rust/signed_doc/examples/mk_signed_doc.rs @@ -199,35 +199,35 @@ fn build_empty_cose_doc(doc_bytes: Vec, meta: &Metadata) -> coset::CoseSign protected_header.rest.push(( coset::Label::Text("type".to_string()), - encode_cbor_uuid(&meta.r#type), + encode_cbor_uuid(&meta.doc_type()), )); protected_header.rest.push(( coset::Label::Text("id".to_string()), - encode_cbor_uuid(&meta.id), + encode_cbor_uuid(&meta.doc_id()), )); protected_header.rest.push(( coset::Label::Text("ver".to_string()), - encode_cbor_uuid(&meta.ver), + encode_cbor_uuid(&meta.doc_ver()), )); - if let Some(r#ref) = &meta.r#ref { + if let Some(r#ref) = &meta.doc_ref() { protected_header.rest.push(( coset::Label::Text("ref".to_string()), encode_cbor_document_ref(r#ref), )); } - if let Some(template) = &meta.template { + if let Some(template) = &meta.doc_template() { protected_header.rest.push(( coset::Label::Text("template".to_string()), encode_cbor_document_ref(template), )); } - if let Some(reply) = &meta.reply { + if let Some(reply) = &meta.doc_reply() { protected_header.rest.push(( coset::Label::Text("reply".to_string()), encode_cbor_document_ref(reply), )); } - if let Some(section) = &meta.section { + if let Some(section) = &meta.doc_section() { protected_header.rest.push(( coset::Label::Text("section".to_string()), coset::cbor::Value::Text(section.clone()), diff --git a/rust/signed_doc/src/lib.rs b/rust/signed_doc/src/lib.rs index bb8a484d5fe..891ae1d6ec8 100644 --- a/rust/signed_doc/src/lib.rs +++ b/rust/signed_doc/src/lib.rs @@ -8,6 +8,10 @@ use std::{ use coset::{CborSerializable, TaggedCborSerializable}; +mod metadata; + +pub use metadata::{DocumentRef, Metadata}; + /// Catalyst Signed Document Content Encoding Key. const CONTENT_ENCODING_KEY: &str = "content encoding"; /// Catalyst Signed Document Content Encoding Value. @@ -59,67 +63,6 @@ struct InnerCatalystSignedDocument { content_errors: Vec, } -/// Document Metadata. -#[derive(Debug, serde::Deserialize)] -pub struct Metadata { - /// Document Type `UUIDv7`. - pub r#type: uuid::Uuid, - /// Document ID `UUIDv7`. - pub id: uuid::Uuid, - /// Document Version `UUIDv7`. - pub ver: uuid::Uuid, - /// Reference to the latest document. - pub r#ref: Option, - /// Reference to the document template. - pub template: Option, - /// Reference to the document reply. - pub reply: Option, - /// Reference to the document section. - 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 { - 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(Copy, Clone, Debug, serde::Deserialize)] -#[serde(untagged)] -pub enum DocumentRef { - /// Reference to the latest document - Latest { - /// Document ID UUID - id: uuid::Uuid, - }, - /// Reference to the specific document version - /// Document ID UUID, Document Ver UUID - WithVer(uuid::Uuid, uuid::Uuid), -} - // Do this instead of `new` if we are converting a single parameter into a struct/type we // should use either `From` or `TryFrom` and reserve `new` for cases where we need // multiple parameters to actually create the type. This is much more elegant to use this @@ -161,9 +104,6 @@ impl TryFrom> for CatalystSignedDocument { } impl CatalystSignedDocument { - /// Invalid Doc Type UUID - 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. /// Are there any validation errors (as opposed to structural errors. @@ -207,6 +147,12 @@ impl CatalystSignedDocument { pub fn doc_reply(&self) -> Option { self.inner.metadata.doc_reply() } + + /// Return Document Reply `Option`. + #[must_use] + pub fn doc_section(&self) -> Option { + self.inner.metadata.doc_section() + } } /// Generate the COSE protected header used by Catalyst Signed Document. diff --git a/rust/signed_doc/src/metadata/document_ref.rs b/rust/signed_doc/src/metadata/document_ref.rs new file mode 100644 index 00000000000..36fab997941 --- /dev/null +++ b/rust/signed_doc/src/metadata/document_ref.rs @@ -0,0 +1,15 @@ +//! Catalyst Signed Document Metadata. + +/// Reference to a Document. +#[derive(Copy, Clone, Debug, serde::Deserialize)] +#[serde(untagged)] +pub enum DocumentRef { + /// Reference to the latest document + Latest { + /// Document ID UUID + id: uuid::Uuid, + }, + /// Reference to the specific document version + /// Document ID UUID, Document Ver UUID + WithVer(uuid::Uuid, uuid::Uuid), +} diff --git a/rust/signed_doc/src/metadata/mod.rs b/rust/signed_doc/src/metadata/mod.rs new file mode 100644 index 00000000000..f3c7c441a80 --- /dev/null +++ b/rust/signed_doc/src/metadata/mod.rs @@ -0,0 +1,99 @@ +//! Catalyst Signed Document Metadata. +use std::fmt::{Display, Formatter}; + +mod document_ref; + +pub use document_ref::DocumentRef; + +/// Document Metadata. +#[derive(Debug, serde::Deserialize)] +pub struct Metadata { + /// Document Type `UUIDv7`. + pub(crate) r#type: uuid::Uuid, + /// Document ID `UUIDv7`. + pub(crate) id: uuid::Uuid, + /// Document Version `UUIDv7`. + pub(crate) ver: uuid::Uuid, + /// Reference to the latest document. + pub(crate) r#ref: Option, + /// Reference to the document template. + pub(crate) template: Option, + /// Reference to the document reply. + pub(crate) reply: Option, + /// Reference to the document section. + pub(crate) section: Option, +} + +impl Metadata { + /// Invalid Doc Type UUID + const INVALID_UUID: uuid::Uuid = uuid::Uuid::from_bytes([0x00; 16]); + + /// Return Document Type `UUIDv4`. + #[must_use] + pub fn doc_type(&self) -> uuid::Uuid { + self.r#type + } + + /// Return Document ID `UUIDv7`. + #[must_use] + pub fn doc_id(&self) -> uuid::Uuid { + self.id + } + + /// Return Document Version `UUIDv7`. + #[must_use] + pub fn doc_ver(&self) -> uuid::Uuid { + self.ver + } + + /// Return Last Document Reference `Option`. + #[must_use] + pub fn doc_ref(&self) -> Option { + self.r#ref + } + + /// Return Document Template `Option`. + #[must_use] + pub fn doc_template(&self) -> Option { + self.template + } + + /// Return Document Reply `Option`. + #[must_use] + pub fn doc_reply(&self) -> Option { + self.reply + } + + /// Return Document Section `Option`. + #[must_use] + pub fn doc_section(&self) -> Option { + self.section.clone() + } +} +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 { + r#type: Self::INVALID_UUID, + id: Self::INVALID_UUID, + ver: Self::INVALID_UUID, + r#ref: None, + template: None, + reply: None, + section: None, + } + } +} From 875aa314be84ab13d0be3b908c5eda6fb9d174da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Rosales?= Date: Wed, 25 Dec 2024 23:17:09 -0600 Subject: [PATCH 15/71] feat(rust/signed-doc): add types for UUIDv4 and UUIDv7 --- rust/signed_doc/src/metadata/mod.rs | 2 + rust/signed_doc/src/metadata/uuid_type/mod.rs | 27 ++++++++ .../src/metadata/uuid_type/uuid_v4.rs | 67 +++++++++++++++++++ .../src/metadata/uuid_type/uuid_v7.rs | 60 +++++++++++++++++ 4 files changed, 156 insertions(+) create mode 100644 rust/signed_doc/src/metadata/uuid_type/mod.rs create mode 100644 rust/signed_doc/src/metadata/uuid_type/uuid_v4.rs create mode 100644 rust/signed_doc/src/metadata/uuid_type/uuid_v7.rs diff --git a/rust/signed_doc/src/metadata/mod.rs b/rust/signed_doc/src/metadata/mod.rs index f3c7c441a80..40750b7daf6 100644 --- a/rust/signed_doc/src/metadata/mod.rs +++ b/rust/signed_doc/src/metadata/mod.rs @@ -2,8 +2,10 @@ use std::fmt::{Display, Formatter}; mod document_ref; +mod uuid_type; pub use document_ref::DocumentRef; +pub use uuid_type::{UuidV4, UuidV7}; /// Document Metadata. #[derive(Debug, serde::Deserialize)] diff --git a/rust/signed_doc/src/metadata/uuid_type/mod.rs b/rust/signed_doc/src/metadata/uuid_type/mod.rs new file mode 100644 index 00000000000..5aac63aa5ab --- /dev/null +++ b/rust/signed_doc/src/metadata/uuid_type/mod.rs @@ -0,0 +1,27 @@ +//! `UUID` types. + +mod uuid_v4; +mod uuid_v7; + +pub use uuid_v4::UuidV4; +pub use uuid_v7::UuidV7; + +/// Invalid Doc Type UUID +pub(crate) const INVALID_UUID: uuid::Uuid = uuid::Uuid::from_bytes([0x00; 16]); + +/// CBOR tag for UUID content. +const UUID_CBOR_TAG: u64 = 37; + +/// Decode `CBOR` encoded `UUID`. +pub(crate) fn decode_cbor_uuid(val: &coset::cbor::Value) -> anyhow::Result { + 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) +} diff --git a/rust/signed_doc/src/metadata/uuid_type/uuid_v4.rs b/rust/signed_doc/src/metadata/uuid_type/uuid_v4.rs new file mode 100644 index 00000000000..efbcbf40f82 --- /dev/null +++ b/rust/signed_doc/src/metadata/uuid_type/uuid_v4.rs @@ -0,0 +1,67 @@ +//! `UUIDv4` Type. +use std::fmt::{Display, Formatter}; + +use super::{decode_cbor_uuid, INVALID_UUID}; + +/// Type representing a `UUIDv4`. +#[derive(Copy, Clone, Debug, serde::Deserialize)] +#[serde(transparent)] +pub struct UuidV4 { + /// UUID + uuid: uuid::Uuid, +} + +impl UuidV4 { + /// Version for `UUIDv4`. + const UUID_VERSION_NUMBER: usize = 4; + + /// Generates a zeroed out `UUIDv4` that can never be valid. + pub fn invalid() -> Self { + Self { uuid: INVALID_UUID } + } + + /// Check if this is a valid `UUIDv4`. + pub fn is_valid(&self) -> bool { + self.uuid != INVALID_UUID || self.uuid.get_version_num() == Self::UUID_VERSION_NUMBER + } + + /// Returns the `uuid::Uuid` type. + #[must_use] + pub fn uuid(&self) -> uuid::Uuid { + self.uuid + } +} + +impl Display for UuidV4 { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { + write!(f, "{}", self.uuid) + } +} + +impl TryFrom<&coset::cbor::Value> for UuidV4 { + type Error = anyhow::Error; + + fn try_from(cbor_value: &coset::cbor::Value) -> Result { + match decode_cbor_uuid(cbor_value) { + Ok(uuid) => { + if uuid.get_version_num() == Self::UUID_VERSION_NUMBER { + Ok(Self { uuid }) + } else { + anyhow::bail!("UUID {uuid} is not `v{}`", Self::UUID_VERSION_NUMBER); + } + }, + Err(e) => { + anyhow::bail!("Invalid UUID. Error: {e}"); + }, + } + } +} + +/// Returns a `UUIDv4` from `uuid::Uuid`. +/// +/// NOTE: This does not guarantee that the `UUID` is valid. +impl From for UuidV4 { + fn from(uuid: uuid::Uuid) -> Self { + Self { uuid } + } +} diff --git a/rust/signed_doc/src/metadata/uuid_type/uuid_v7.rs b/rust/signed_doc/src/metadata/uuid_type/uuid_v7.rs new file mode 100644 index 00000000000..e61e538791c --- /dev/null +++ b/rust/signed_doc/src/metadata/uuid_type/uuid_v7.rs @@ -0,0 +1,60 @@ +//! `UUIDv7` Type. +use std::fmt::{Display, Formatter}; + +use super::{decode_cbor_uuid, INVALID_UUID}; + +/// Type representing a `UUIDv7`. +#[derive(Copy, Clone, Debug, serde::Deserialize, PartialEq, PartialOrd)] +#[serde(transparent)] +pub struct UuidV7 { + /// UUID + uuid: uuid::Uuid, +} + +impl UuidV7 { + /// Version for `UUIDv7`. + const UUID_VERSION_NUMBER: usize = 7; + + /// Generates a zeroed out `UUIDv7` that can never be valid. + #[must_use] + pub fn invalid() -> Self { + Self { uuid: INVALID_UUID } + } + + /// Check if this is a valid `UUIDv7`. + #[must_use] + pub fn is_valid(&self) -> bool { + self.uuid != INVALID_UUID || self.uuid.get_version_num() == Self::UUID_VERSION_NUMBER + } + + /// Returns the `uuid::Uuid` type. + #[must_use] + pub fn uuid(&self) -> uuid::Uuid { + self.uuid + } +} + +impl Display for UuidV7 { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { + write!(f, "{}", self.uuid) + } +} + +impl TryFrom<&coset::cbor::Value> for UuidV7 { + type Error = anyhow::Error; + + fn try_from(cbor_value: &coset::cbor::Value) -> Result { + match decode_cbor_uuid(cbor_value) { + Ok(uuid) => { + if uuid.get_version_num() == Self::UUID_VERSION_NUMBER { + Ok(Self { uuid }) + } else { + anyhow::bail!("UUID {uuid} is not `v{}`", Self::UUID_VERSION_NUMBER); + } + }, + Err(e) => { + anyhow::bail!("Invalid UUID. Error: {e}"); + }, + } + } +} From 7e2425d5440063c104795baab809b13bcdea05af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Rosales?= Date: Wed, 25 Dec 2024 23:20:23 -0600 Subject: [PATCH 16/71] fix(rust/signed_doc): update code and examples with Uuid types --- rust/signed_doc/examples/mk_signed_doc.rs | 15 ++++++---- rust/signed_doc/src/metadata/document_ref.rs | 5 ++-- rust/signed_doc/src/metadata/mod.rs | 31 +++++++++----------- 3 files changed, 26 insertions(+), 25 deletions(-) diff --git a/rust/signed_doc/examples/mk_signed_doc.rs b/rust/signed_doc/examples/mk_signed_doc.rs index 71da84cd858..e892354d0ef 100644 --- a/rust/signed_doc/examples/mk_signed_doc.rs +++ b/rust/signed_doc/examples/mk_signed_doc.rs @@ -14,7 +14,7 @@ use ed25519_dalek::{ ed25519::signature::Signer, pkcs8::{DecodePrivateKey, DecodePublicKey}, }; -use signed_doc::{DocumentRef, Metadata}; +use signed_doc::{DocumentRef, Metadata, UuidV7}; fn main() { if let Err(err) = Cli::parse().exec() { @@ -83,24 +83,27 @@ fn decode_cbor_uuid(val: &coset::cbor::Value) -> anyhow::Result { fn encode_cbor_document_ref(doc_ref: &DocumentRef) -> coset::cbor::Value { match doc_ref { - DocumentRef::Latest { id } => encode_cbor_uuid(id), + DocumentRef::Latest { id } => encode_cbor_uuid(&id.uuid()), DocumentRef::WithVer(id, ver) => { - coset::cbor::Value::Array(vec![encode_cbor_uuid(id), encode_cbor_uuid(ver)]) + coset::cbor::Value::Array(vec![ + encode_cbor_uuid(&id.uuid()), + encode_cbor_uuid(&ver.uuid()), + ]) }, } } #[allow(clippy::indexing_slicing)] fn decode_cbor_document_ref(val: &coset::cbor::Value) -> anyhow::Result { - if let Ok(id) = decode_cbor_uuid(val) { + if let Ok(id) = UuidV7::try_from(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])?; + let id = UuidV7::try_from(&array[0])?; + let ver = UuidV7::try_from(&array[1])?; Ok(DocumentRef::WithVer(id, ver)) } } diff --git a/rust/signed_doc/src/metadata/document_ref.rs b/rust/signed_doc/src/metadata/document_ref.rs index 36fab997941..5e0e04968a6 100644 --- a/rust/signed_doc/src/metadata/document_ref.rs +++ b/rust/signed_doc/src/metadata/document_ref.rs @@ -1,4 +1,5 @@ //! Catalyst Signed Document Metadata. +use super::UuidV7; /// Reference to a Document. #[derive(Copy, Clone, Debug, serde::Deserialize)] @@ -7,9 +8,9 @@ pub enum DocumentRef { /// Reference to the latest document Latest { /// Document ID UUID - id: uuid::Uuid, + id: UuidV7, }, /// Reference to the specific document version /// Document ID UUID, Document Ver UUID - WithVer(uuid::Uuid, uuid::Uuid), + WithVer(UuidV7, UuidV7), } diff --git a/rust/signed_doc/src/metadata/mod.rs b/rust/signed_doc/src/metadata/mod.rs index 40750b7daf6..9823f91e71a 100644 --- a/rust/signed_doc/src/metadata/mod.rs +++ b/rust/signed_doc/src/metadata/mod.rs @@ -10,42 +10,39 @@ pub use uuid_type::{UuidV4, UuidV7}; /// Document Metadata. #[derive(Debug, serde::Deserialize)] pub struct Metadata { - /// Document Type `UUIDv7`. - pub(crate) r#type: uuid::Uuid, + /// Document Type `UUIDv4`. + r#type: UuidV4, /// Document ID `UUIDv7`. - pub(crate) id: uuid::Uuid, + id: UuidV7, /// Document Version `UUIDv7`. - pub(crate) ver: uuid::Uuid, + ver: UuidV7, /// Reference to the latest document. - pub(crate) r#ref: Option, + r#ref: Option, /// Reference to the document template. - pub(crate) template: Option, + template: Option, /// Reference to the document reply. - pub(crate) reply: Option, + reply: Option, /// Reference to the document section. - pub(crate) section: Option, + section: Option, } impl Metadata { - /// Invalid Doc Type UUID - const INVALID_UUID: uuid::Uuid = uuid::Uuid::from_bytes([0x00; 16]); - /// Return Document Type `UUIDv4`. #[must_use] pub fn doc_type(&self) -> uuid::Uuid { - self.r#type + self.r#type.uuid() } /// Return Document ID `UUIDv7`. #[must_use] pub fn doc_id(&self) -> uuid::Uuid { - self.id + self.id.uuid() } /// Return Document Version `UUIDv7`. #[must_use] pub fn doc_ver(&self) -> uuid::Uuid { - self.ver + self.ver.uuid() } /// Return Last Document Reference `Option`. @@ -89,9 +86,9 @@ impl Display for Metadata { impl Default for Metadata { fn default() -> Self { Self { - r#type: Self::INVALID_UUID, - id: Self::INVALID_UUID, - ver: Self::INVALID_UUID, + r#type: UuidV4::invalid(), + id: UuidV7::invalid(), + ver: UuidV7::invalid(), r#ref: None, template: None, reply: None, From c28635dc65ecf584486d56d034fde300d0f5d29f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Rosales?= Date: Wed, 25 Dec 2024 23:22:40 -0600 Subject: [PATCH 17/71] fix(rust/signed_doc): refactor Metadata impl TryFrom --- rust/signed_doc/src/lib.rs | 213 ++++------------------------ rust/signed_doc/src/metadata/mod.rs | 167 ++++++++++++++++++++++ 2 files changed, 192 insertions(+), 188 deletions(-) diff --git a/rust/signed_doc/src/lib.rs b/rust/signed_doc/src/lib.rs index 891ae1d6ec8..201d12a35c4 100644 --- a/rust/signed_doc/src/lib.rs +++ b/rust/signed_doc/src/lib.rs @@ -10,17 +10,12 @@ use coset::{CborSerializable, TaggedCborSerializable}; mod metadata; -pub use metadata::{DocumentRef, Metadata}; +pub use metadata::{DocumentRef, Metadata, UuidV7}; /// 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); /// Keep all the contents private. /// Better even to use a structure like this. Wrapping in an Arc means we don't have to @@ -75,8 +70,26 @@ impl TryFrom> for CatalystSignedDocument { let cose = coset::CoseSign::from_tagged_slice(&cose_bytes) .or(coset::CoseSign::from_slice(&cose_bytes)) .map_err(|e| anyhow::anyhow!("Invalid COSE Sign document: {e}"))?; + let mut content_errors = Vec::new(); + let expected_header = cose_protected_header(); + + if cose.protected.header.content_type != expected_header.content_type { + content_errors + .push("Invalid COSE document protected header `content-type` field".to_string()); + } - let (metadata, content_errors) = metadata_from_cose_protected_header(&cose); + if !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()) + }) { + content_errors.push( + "Invalid COSE document protected header {CONTENT_ENCODING_KEY} field".to_string(), + ); + } + let metadata = Metadata::from(&cose.protected); + if metadata.has_error() { + content_errors.extend_from_slice(metadata.content_errors()); + } let payload = match &cose.payload { Some(payload) => { let mut buf = Vec::new(); @@ -95,7 +108,7 @@ impl TryFrom> for CatalystSignedDocument { payload, signatures, cose_sign: cose, - content_errors: content_errors.0, + content_errors, }; Ok(CatalystSignedDocument { inner: Arc::new(inner), @@ -106,7 +119,7 @@ impl TryFrom> for CatalystSignedDocument { impl CatalystSignedDocument { // A bunch of getters to access the contents, or reason through the document, such as. - /// Are there any validation errors (as opposed to structural errors. + /// Are there any validation errors (as opposed to structural errors). #[must_use] pub fn has_error(&self) -> bool { !self.inner.content_errors.is_empty() @@ -166,38 +179,10 @@ fn cose_protected_header() -> coset::Header { .build() } -/// Decode `CBOR` encoded `UUID`. -fn decode_cbor_uuid(val: &coset::cbor::Value) -> anyhow::Result { - 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 { - 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)) - } -} - /// Find a value for a given key in the protected header. -fn cose_protected_header_find(cose: &coset::CoseSign, rest_key: &str) -> Option { +fn cose_protected_header_find( + cose: &coset::CoseSign, rest_key: &str, +) -> Option { cose.protected .header .rest @@ -205,151 +190,3 @@ fn cose_protected_header_find(cose: &coset::CoseSign, rest_key: &str) -> Option< .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) -> (Metadata, ContentErrors) { - let expected_header = cose_protected_header(); - let mut errors = Vec::new(); - - if cose.protected.header.content_type != expected_header.content_type { - errors.push("Invalid COSE document protected header `content-type` field".to_string()); - } - - if !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()) - }) { - errors.push( - "Invalid COSE document protected header {CONTENT_ENCODING_KEY} field".to_string(), - ); - } - let mut metadata = Metadata::default(); - - 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; - } else { - errors.push(format!( - "Document type is not a valid UUIDv4: {doc_type_uuid}" - )); - } - }, - Err(e) => { - errors.push(format!( - "Invalid COSE protected header `type` field, err: {e}" - )); - }, - } - }, - None => errors.push("Invalid COSE protected header, missing `type` field".to_string()), - }; - - 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; - } else { - errors.push(format!("Document ID is not a valid UUIDv7: {doc_id_uuid}")); - } - }, - Err(e) => { - errors.push(format!( - "Invalid COSE protected header `id` field, err: {e}" - )); - }, - } - }, - None => errors.push("Invalid COSE protected header, missing `id` field".to_string()), - }; - - 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 { - errors.push(format!( - "Document Version is not a valid UUIDv7: {doc_ver_uuid}" - )); - is_valid = false; - } - if doc_ver_uuid < metadata.id { - errors.push(format!( - "Document Version {doc_ver_uuid} cannot be smaller than Document ID {0}", metadata.id - )); - is_valid = false; - } - if is_valid { - metadata.ver = doc_ver_uuid; - } - }, - Err(e) => { - errors.push(format!( - "Invalid COSE protected header `ver` field, err: {e}" - )); - }, - } - }, - None => errors.push("Invalid COSE protected header, missing `ver` field".to_string()), - } - - 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(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(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(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:?}" - )); - }, - } - } - - (metadata, ContentErrors(errors)) -} diff --git a/rust/signed_doc/src/metadata/mod.rs b/rust/signed_doc/src/metadata/mod.rs index 9823f91e71a..a578cff47cd 100644 --- a/rust/signed_doc/src/metadata/mod.rs +++ b/rust/signed_doc/src/metadata/mod.rs @@ -24,6 +24,9 @@ pub struct Metadata { reply: Option, /// Reference to the document section. section: Option, + /// Metadata Content Errors + #[serde(skip)] + content_errors: Vec, } impl Metadata { @@ -68,6 +71,18 @@ impl Metadata { pub fn doc_section(&self) -> Option { self.section.clone() } + + /// Are there any validation errors (as opposed to structural errors). + #[must_use] + pub fn has_error(&self) -> bool { + !self.content_errors.is_empty() + } + + /// List of Content Errors. + #[must_use] + pub fn content_errors(&self) -> &Vec { + &self.content_errors + } } impl Display for Metadata { fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { @@ -93,6 +108,158 @@ impl Default for Metadata { template: None, reply: None, section: None, + content_errors: Vec::new(), + } + } +} + +/// Errors found when decoding content. +#[derive(Default, Debug)] +struct ContentErrors(Vec); + +impl ContentErrors { + /// Appends an element to the back of the collection + fn push(&mut self, error_string: String) { + self.0.push(error_string); + } +} + +impl From<&coset::ProtectedHeader> for Metadata { + #[allow(clippy::too_many_lines)] + fn from(protected: &coset::ProtectedHeader) -> Self { + let mut metadata = Metadata::default(); + let mut errors = Vec::new(); + + match cose_protected_header_find(protected, "type") { + Some(doc_type) => { + match UuidV4::try_from(&doc_type) { + Ok(doc_type_uuid) => { + metadata.r#type = doc_type_uuid; + }, + Err(e) => { + errors.push(format!("Document `type` is invalid: {e}")); + }, + } + }, + None => errors.push("Invalid COSE protected header, missing `type` field".to_string()), + }; + + match cose_protected_header_find(protected, "id") { + Some(doc_id) => { + match UuidV7::try_from(&doc_id) { + Ok(doc_id_uuid) => { + metadata.id = doc_id_uuid; + }, + Err(e) => { + errors.push(format!("Document `id` is invalid: {e}")); + }, + } + }, + None => errors.push("Invalid COSE protected header, missing `id` field".to_string()), + }; + + match cose_protected_header_find(protected, "ver") { + Some(doc_ver) => { + match UuidV7::try_from(&doc_ver) { + Ok(doc_ver_uuid) => { + if doc_ver_uuid < metadata.id { + errors.push(format!( + "Document Version {doc_ver_uuid} cannot be smaller than Document ID {}", metadata.id + )); + } else { + metadata.ver = doc_ver_uuid; + } + }, + Err(e) => { + errors.push(format!( + "Invalid COSE protected header `ver` field, err: {e}" + )); + }, + } + }, + None => errors.push("Invalid COSE protected header, missing `ver` field".to_string()), + } + + if let Some(cbor_doc_ref) = cose_protected_header_find(protected, "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(cbor_doc_template) = cose_protected_header_find(protected, "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(cbor_doc_reply) = cose_protected_header_find(protected, "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(cbor_doc_section) = cose_protected_header_find(protected, "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:?}" + )); + }, + } + } + metadata.content_errors = errors; + metadata + } +} + +/// Find a value for a given key in the protected header. +fn cose_protected_header_find( + protected: &coset::ProtectedHeader, rest_key: &str, +) -> Option { + protected + .header + .rest + .iter() + .find(|(key, _)| key == &coset::Label::Text(rest_key.to_string())) + .map(|(_, value)| value.clone()) +} + +/// Decode `CBOR` encoded `DocumentRef`. +#[allow(clippy::indexing_slicing)] +fn decode_cbor_document_ref(val: &coset::cbor::Value) -> anyhow::Result { + if let Ok(id) = UuidV7::try_from(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 = UuidV7::try_from(&array[0])?; + let ver = UuidV7::try_from(&array[1])?; + Ok(DocumentRef::WithVer(id, ver)) } } From 8002c053307b12aa7dddfbd1a2877c09060d47cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Rosales?= Date: Wed, 25 Dec 2024 23:59:52 -0600 Subject: [PATCH 18/71] fix(rust/signed_doc): refactor impl TryFrom<&coset::cbor::Value> for DocumentRef * fix UuidV4 and UuidV7 is_valid methods --- rust/signed_doc/src/metadata/document_ref.rs | 26 +++++++++++++++++++ rust/signed_doc/src/metadata/mod.rs | 22 +++------------- .../src/metadata/uuid_type/uuid_v4.rs | 2 +- .../src/metadata/uuid_type/uuid_v7.rs | 2 +- 4 files changed, 31 insertions(+), 21 deletions(-) diff --git a/rust/signed_doc/src/metadata/document_ref.rs b/rust/signed_doc/src/metadata/document_ref.rs index 5e0e04968a6..114226b5405 100644 --- a/rust/signed_doc/src/metadata/document_ref.rs +++ b/rust/signed_doc/src/metadata/document_ref.rs @@ -14,3 +14,29 @@ pub enum DocumentRef { /// Document ID UUID, Document Ver UUID WithVer(UuidV7, UuidV7), } + +impl TryFrom<&coset::cbor::Value> for DocumentRef { + type Error = anyhow::Error; + + #[allow(clippy::indexing_slicing)] + fn try_from(val: &coset::cbor::Value) -> anyhow::Result { + if let Ok(id) = UuidV7::try_from(val) { + Ok(DocumentRef::Latest { id }) + } else { + let Some(array) = val.as_array() else { + anyhow::bail!("Document Reference must be either a single UUID or an array of two"); + }; + anyhow::ensure!( + array.len() == 2, + "Document Reference array of two UUIDs was expected" + ); + let id = UuidV7::try_from(&array[0])?; + let ver = UuidV7::try_from(&array[1])?; + anyhow::ensure!( + ver >= id, + "Document Reference Version can never be smaller than its ID" + ); + Ok(DocumentRef::WithVer(id, ver)) + } + } +} diff --git a/rust/signed_doc/src/metadata/mod.rs b/rust/signed_doc/src/metadata/mod.rs index a578cff47cd..c6cb313cc6e 100644 --- a/rust/signed_doc/src/metadata/mod.rs +++ b/rust/signed_doc/src/metadata/mod.rs @@ -181,7 +181,7 @@ impl From<&coset::ProtectedHeader> for Metadata { } if let Some(cbor_doc_ref) = cose_protected_header_find(protected, "ref") { - match decode_cbor_document_ref(&cbor_doc_ref) { + match DocumentRef::try_from(&cbor_doc_ref) { Ok(doc_ref) => { metadata.r#ref = Some(doc_ref); }, @@ -194,7 +194,7 @@ impl From<&coset::ProtectedHeader> for Metadata { } if let Some(cbor_doc_template) = cose_protected_header_find(protected, "template") { - match decode_cbor_document_ref(&cbor_doc_template) { + match DocumentRef::try_from(&cbor_doc_template) { Ok(doc_template) => { metadata.template = Some(doc_template); }, @@ -207,7 +207,7 @@ impl From<&coset::ProtectedHeader> for Metadata { } if let Some(cbor_doc_reply) = cose_protected_header_find(protected, "reply") { - match decode_cbor_document_ref(&cbor_doc_reply) { + match DocumentRef::try_from(&cbor_doc_reply) { Ok(doc_reply) => { metadata.reply = Some(doc_reply); }, @@ -247,19 +247,3 @@ fn cose_protected_header_find( .find(|(key, _)| key == &coset::Label::Text(rest_key.to_string())) .map(|(_, value)| value.clone()) } - -/// Decode `CBOR` encoded `DocumentRef`. -#[allow(clippy::indexing_slicing)] -fn decode_cbor_document_ref(val: &coset::cbor::Value) -> anyhow::Result { - if let Ok(id) = UuidV7::try_from(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 = UuidV7::try_from(&array[0])?; - let ver = UuidV7::try_from(&array[1])?; - Ok(DocumentRef::WithVer(id, ver)) - } -} diff --git a/rust/signed_doc/src/metadata/uuid_type/uuid_v4.rs b/rust/signed_doc/src/metadata/uuid_type/uuid_v4.rs index efbcbf40f82..95084221076 100644 --- a/rust/signed_doc/src/metadata/uuid_type/uuid_v4.rs +++ b/rust/signed_doc/src/metadata/uuid_type/uuid_v4.rs @@ -22,7 +22,7 @@ impl UuidV4 { /// Check if this is a valid `UUIDv4`. pub fn is_valid(&self) -> bool { - self.uuid != INVALID_UUID || self.uuid.get_version_num() == Self::UUID_VERSION_NUMBER + self.uuid != INVALID_UUID && self.uuid.get_version_num() == Self::UUID_VERSION_NUMBER } /// Returns the `uuid::Uuid` type. diff --git a/rust/signed_doc/src/metadata/uuid_type/uuid_v7.rs b/rust/signed_doc/src/metadata/uuid_type/uuid_v7.rs index e61e538791c..bd1a2a00bcb 100644 --- a/rust/signed_doc/src/metadata/uuid_type/uuid_v7.rs +++ b/rust/signed_doc/src/metadata/uuid_type/uuid_v7.rs @@ -24,7 +24,7 @@ impl UuidV7 { /// Check if this is a valid `UUIDv7`. #[must_use] pub fn is_valid(&self) -> bool { - self.uuid != INVALID_UUID || self.uuid.get_version_num() == Self::UUID_VERSION_NUMBER + self.uuid != INVALID_UUID && self.uuid.get_version_num() == Self::UUID_VERSION_NUMBER } /// Returns the `uuid::Uuid` type. From 5c88eb7c7867ab2e61c1620a8256900757eeafa0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Rosales?= Date: Sun, 29 Dec 2024 12:23:00 -0600 Subject: [PATCH 19/71] fix(rust/signed-doc): remove unused dependency --- rust/signed_doc/Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/rust/signed_doc/Cargo.toml b/rust/signed_doc/Cargo.toml index aa41c00b364..44b45f15110 100644 --- a/rust/signed_doc/Cargo.toml +++ b/rust/signed_doc/Cargo.toml @@ -15,7 +15,6 @@ 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"] } From 2d61209b13025c7364e3e83121f1e3b07768e55f Mon Sep 17 00:00:00 2001 From: Steven Johnson Date: Fri, 3 Jan 2025 13:01:41 +0700 Subject: [PATCH 20/71] fix(rust): Fix cargo.toml broken after merge --- rust/signed_doc/Cargo.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/rust/signed_doc/Cargo.toml b/rust/signed_doc/Cargo.toml index 55791b172e2..b4e47d03137 100644 --- a/rust/signed_doc/Cargo.toml +++ b/rust/signed_doc/Cargo.toml @@ -13,8 +13,7 @@ workspace = true [dependencies] anyhow = "1.0.95" serde = { version = "1.0.217", features = ["derive"] } -serde_json = "1.0.134"serde = { version = "1.0.210", features = ["derive"] } -serde_json = "1.0" +serde_json = "1.0.134" # TODO: Bump this to the latest version and fix the code jsonschema = "0.18.3" coset = "0.3.8" From 13b7ac7dca12573e4a417622878965fc3801de7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Rosales?= Date: Fri, 3 Jan 2025 12:02:14 -0600 Subject: [PATCH 21/71] fix(rust/signed-doc): Update rust/signed_doc/src/lib.rs Correct name for Content-Encoding. Co-authored-by: Steven Johnson --- rust/signed_doc/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/signed_doc/src/lib.rs b/rust/signed_doc/src/lib.rs index 201d12a35c4..4fd2c220ec0 100644 --- a/rust/signed_doc/src/lib.rs +++ b/rust/signed_doc/src/lib.rs @@ -13,7 +13,7 @@ mod metadata; pub use metadata::{DocumentRef, Metadata, UuidV7}; /// Catalyst Signed Document Content Encoding Key. -const CONTENT_ENCODING_KEY: &str = "content encoding"; +const CONTENT_ENCODING_KEY: &str = "Content-Encoding"; /// Catalyst Signed Document Content Encoding Value. const CONTENT_ENCODING_VALUE: &str = "br"; From 5cd299fb255e968d87721ab7cf7b16208087b582 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Rosales?= Date: Fri, 3 Jan 2025 16:18:16 -0600 Subject: [PATCH 22/71] feat(rust/signed-doc): add type for Key ID * update example + tidy up metadata module --- rust/signed_doc/examples/mk_signed_doc.rs | 24 +++- rust/signed_doc/src/lib.rs | 13 +- rust/signed_doc/src/metadata/mod.rs | 1 + .../signed_doc/src/signature/kid/authority.rs | 37 ++++++ .../src/signature/kid/key_version.rs | 19 +++ rust/signed_doc/src/signature/kid/mod.rs | 114 ++++++++++++++++++ rust/signed_doc/src/signature/kid/role.rs | 42 +++++++ rust/signed_doc/src/signature/kid/role0_pk.rs | 40 ++++++ rust/signed_doc/src/signature/mod.rs | 4 + 9 files changed, 286 insertions(+), 8 deletions(-) create mode 100644 rust/signed_doc/src/signature/kid/authority.rs create mode 100644 rust/signed_doc/src/signature/kid/key_version.rs create mode 100644 rust/signed_doc/src/signature/kid/mod.rs create mode 100644 rust/signed_doc/src/signature/kid/role.rs create mode 100644 rust/signed_doc/src/signature/kid/role0_pk.rs create mode 100644 rust/signed_doc/src/signature/mod.rs diff --git a/rust/signed_doc/examples/mk_signed_doc.rs b/rust/signed_doc/examples/mk_signed_doc.rs index e892354d0ef..12cab376a9f 100644 --- a/rust/signed_doc/examples/mk_signed_doc.rs +++ b/rust/signed_doc/examples/mk_signed_doc.rs @@ -6,6 +6,7 @@ use std::{ fs::{read_to_string, File}, io::{Read, Write}, path::PathBuf, + str::FromStr, }; use clap::Parser; @@ -14,7 +15,7 @@ use ed25519_dalek::{ ed25519::signature::Signer, pkcs8::{DecodePrivateKey, DecodePublicKey}, }; -use signed_doc::{DocumentRef, Metadata, UuidV7}; +use signed_doc::{DocumentRef, Kid, Metadata, UuidV7}; fn main() { if let Err(err) = Cli::parse().exec() { @@ -132,9 +133,13 @@ impl Cli { store_cose_file(cose, &doc)?; }, Self::Verify { pk, doc, schema } => { - let pk = load_public_key_from_file(&pk)?; - let schema = load_schema_from_file(&schema)?; - let cose = load_cose_from_file(&doc)?; + let pk = load_public_key_from_file(&pk) + .map_err(|e| anyhow::anyhow!("Failed to load public key from file: {e}"))?; + let schema = load_schema_from_file(&schema).map_err(|e| { + anyhow::anyhow!("Failed to load document schema from file: {e}") + })?; + let cose = load_cose_from_file(&doc) + .map_err(|e| anyhow::anyhow!("Failed to load COSE SIGN from file: {e}"))?; validate_cose(&cose, &pk, &schema)?; println!("Document is valid."); }, @@ -294,11 +299,15 @@ fn validate_cose( validate_json(&json_doc, schema)?; for sign in &cose.signatures { + let key_id = sign.protected.header.key_id.clone(); anyhow::ensure!( - !sign.protected.header.key_id.is_empty(), + !key_id.is_empty(), "COSE missing signature protected header `kid` field " ); + let kid_str = String::from_utf8_lossy(&key_id); + let kid = Kid::from_str(&kid_str)?; + println!("Signature Key ID: {kid}"); let data_to_sign = cose.tbs_data(&[], sign); let signature_bytes = sign.signature.as_slice().try_into().map_err(|_| { anyhow::anyhow!( @@ -307,6 +316,11 @@ fn validate_cose( sign.signature.len() ) })?; + println!( + "Verifying Key Len({}): 0x{}", + pk.as_bytes().len(), + hex::encode(pk.as_bytes()) + ); let signature = ed25519_dalek::Signature::from_bytes(signature_bytes); pk.verify_strict(&data_to_sign, &signature)?; } diff --git a/rust/signed_doc/src/lib.rs b/rust/signed_doc/src/lib.rs index 4fd2c220ec0..41ff63dda0e 100644 --- a/rust/signed_doc/src/lib.rs +++ b/rust/signed_doc/src/lib.rs @@ -9,8 +9,10 @@ use std::{ use coset::{CborSerializable, TaggedCborSerializable}; mod metadata; +mod signature; pub use metadata::{DocumentRef, Metadata, UuidV7}; +pub use signature::Kid; /// Catalyst Signed Document Content Encoding Key. const CONTENT_ENCODING_KEY: &str = "Content-Encoding"; @@ -30,9 +32,14 @@ impl Display for CatalystSignedDocument { fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { writeln!(f, "{}", self.inner.metadata)?; writeln!(f, "JSON Payload {:#}\n", self.inner.payload)?; - writeln!(f, "Signatures [")?; + writeln!(f, "Signature Information [")?; for signature in &self.inner.signatures { - writeln!(f, " 0x{:#}", hex::encode(signature.signature.as_slice()))?; + writeln!( + f, + " {} 0x{:#}", + String::from_utf8_lossy(&signature.protected.header.key_id), + hex::encode(signature.signature.as_slice()) + )?; } writeln!(f, "]\n")?; writeln!(f, "Content Errors [")?; @@ -52,7 +59,7 @@ struct InnerCatalystSignedDocument { payload: serde_json::Value, /// Signatures signatures: Vec, - /// Raw COSE Sign bytes + /// Raw COSE Sign data cose_sign: coset::CoseSign, /// Content Errors found when parsing the Document content_errors: Vec, diff --git a/rust/signed_doc/src/metadata/mod.rs b/rust/signed_doc/src/metadata/mod.rs index c6cb313cc6e..b9c10a2d33b 100644 --- a/rust/signed_doc/src/metadata/mod.rs +++ b/rust/signed_doc/src/metadata/mod.rs @@ -84,6 +84,7 @@ impl Metadata { &self.content_errors } } + impl Display for Metadata { fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { writeln!(f, "Metadata {{")?; diff --git a/rust/signed_doc/src/signature/kid/authority.rs b/rust/signed_doc/src/signature/kid/authority.rs new file mode 100644 index 00000000000..e2ab0cb182e --- /dev/null +++ b/rust/signed_doc/src/signature/kid/authority.rs @@ -0,0 +1,37 @@ +//! COSE Signature Protected Header `kid` URI Authority. + +use std::{ + fmt::{Display, Formatter}, + str::FromStr, +}; + +/// URI Authority +#[derive(Debug, Clone)] +pub enum Authority { + /// Cardano Blockchain + Cardano, + /// Midnight Blockchain + Midnight, +} + +impl FromStr for Authority { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + match s { + "cardano" => Ok(Authority::Cardano), + "midnight" => Ok(Authority::Midnight), + _ => Err(anyhow::anyhow!("Unknown Authority: {s}")), + } + } +} + +impl Display for Authority { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { + let authority = match self { + Self::Cardano => "cardano", + Self::Midnight => "midnight", + }; + write!(f, "{authority}") + } +} diff --git a/rust/signed_doc/src/signature/kid/key_version.rs b/rust/signed_doc/src/signature/kid/key_version.rs new file mode 100644 index 00000000000..7314af0b04d --- /dev/null +++ b/rust/signed_doc/src/signature/kid/key_version.rs @@ -0,0 +1,19 @@ +//! COSE Signature Protected Header `kid` Role0 Key Version. + +use std::fmt::{Display, Formatter}; + +/// Version of the Role0 Key. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct KeyVersion(u16); + +impl From for KeyVersion { + fn from(value: u16) -> Self { + Self(value) + } +} + +impl Display for KeyVersion { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { + write!(f, "{}", self.0) + } +} diff --git a/rust/signed_doc/src/signature/kid/mod.rs b/rust/signed_doc/src/signature/kid/mod.rs new file mode 100644 index 00000000000..e0984a1586b --- /dev/null +++ b/rust/signed_doc/src/signature/kid/mod.rs @@ -0,0 +1,114 @@ +//! COSE Signature Protected Header `kid`. +mod authority; +mod key_version; +mod role; +mod role0_pk; + +use std::{ + fmt::{Display, Formatter}, + str::FromStr, +}; + +use authority::Authority; +use key_version::KeyVersion; +use role::Role; +use role0_pk::Role0PublicKey; + +/// Catalyst Signed Document Key ID +/// +/// Key ID associated with a `COSE` Signature that is structured as a Universal Resource +/// Identifier (`URI`). +#[derive(Debug, Clone)] +pub struct Kid { + /// URI Authority + authority: Authority, + /// Role0 Public Key. + role0_public_key: Role0PublicKey, + /// User Role specified for the current document. + role: Role, + /// Role0 Public Key Version + key_version: KeyVersion, +} + +impl Kid { + /// URI scheme for Catalyst + const URI_SCHEME_PREFIX: &str = "catalyst_kid://"; +} + +impl FromStr for Kid { + type Err = anyhow::Error; + + fn from_str(s: &str) -> anyhow::Result { + let Some(uri) = s.strip_prefix(Self::URI_SCHEME_PREFIX) else { + anyhow::bail!("Key ID scheme must be '{}': {s}", Self::URI_SCHEME_PREFIX); + }; + + let Some((authority_str, key_role_version)) = uri.split_once('/') else { + anyhow::bail!("Key ID must have an authority: {uri}"); + }; + + let authority = Authority::from_str(authority_str) + .map_err(|e| anyhow::anyhow!("Invalid Authority: {authority_str}. {e}"))?; + + let Some((role0_key_str, role_version)) = key_role_version.split_once('/') else { + anyhow::bail!("Expected Key ID have an Role0 Key set: {key_role_version}"); + }; + + let role0_public_key = Role0PublicKey::from_str(role0_key_str) + .map_err(|e| anyhow::anyhow!("Invalid Role0 Public Key: {role0_key_str}. {e}"))?; + + let Some((role_str, key_version_str)) = role_version.split_once('/') else { + anyhow::bail!("Expected Key ID have a role set"); + }; + + let role = Role::from_str(role_str) + .map_err(|e| anyhow::anyhow!("Invalid Role: {role_str}. {e}"))?; + + let key_version: KeyVersion = u16::from_str(key_version_str) + .map_err(|e| anyhow::anyhow!("Invalid Key Version: {key_version_str}. {e}"))? + .into(); + + Ok(Kid { + authority, + role0_public_key, + role, + key_version, + }) + } +} + +impl Display for Kid { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { + write!( + f, + "{}{}/{}/{}/{}", + Self::URI_SCHEME_PREFIX, + self.authority, + self.role0_public_key, + self.role, + self.key_version, + ) + } +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use super::Kid; + + const KID_STR: &str = "catalyst_kid://cardano/0x0063ce08eccfdd5c93dd5cc9ca959fe669fd762fa816d70438efa90c0a75288c/3/0"; + + #[test] + fn test_kid_uri_from_str() { + let kid_str = KID_STR; + assert!(Kid::from_str(kid_str).is_ok()); + } + + #[test] + fn test_kid_uri_from_str_and_back() { + let kid_str = KID_STR; + let kid = Kid::from_str(kid_str).unwrap(); + assert_eq!(KID_STR, format!("{kid}")); + } +} diff --git a/rust/signed_doc/src/signature/kid/role.rs b/rust/signed_doc/src/signature/kid/role.rs new file mode 100644 index 00000000000..b69b6552803 --- /dev/null +++ b/rust/signed_doc/src/signature/kid/role.rs @@ -0,0 +1,42 @@ +//! COSE Signature Protected Header `kid` URI Catalyst User Role. + +use std::{ + fmt::{Display, Formatter}, + str::FromStr, +}; + +/// Project Catalyst User Role associated with the signature. +/// +/// +#[repr(u16)] +#[derive(Debug, Copy, Clone)] +pub enum Role { + /// Voter = 0 + Zero, + /// Delegated Representative = 1 + One, + /// Voter Delegation = 2 + Two, + /// Proposer = 3 + Three, +} + +impl FromStr for Role { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + match s { + "0" => Ok(Role::Zero), + "1" => Ok(Role::One), + "2" => Ok(Role::Two), + "3" => Ok(Role::Three), + _ => Err(anyhow::anyhow!("Unknown Role: {}", s)), + } + } +} + +impl Display for Role { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { + write!(f, "{}", *self as u16) + } +} diff --git a/rust/signed_doc/src/signature/kid/role0_pk.rs b/rust/signed_doc/src/signature/kid/role0_pk.rs new file mode 100644 index 00000000000..4557807fea7 --- /dev/null +++ b/rust/signed_doc/src/signature/kid/role0_pk.rs @@ -0,0 +1,40 @@ +//! COSE Signature Protected Header `kid` URI Role0 Public Key. + +use std::{ + fmt::{Display, Formatter}, + str::FromStr, +}; + +/// Role0 Public Key. +#[derive(Debug, Clone)] +pub struct Role0PublicKey([u8; 32]); + +impl FromStr for Role0PublicKey { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + let Some(role0_hex) = s.strip_prefix("0x") else { + anyhow::bail!("Role0 Public Key hex string must start with '0x': {}", s); + }; + let role0_key = hex::decode(role0_hex) + .map_err(|e| anyhow::anyhow!("Role0 Public Key is not a valid hex string: {}", e))?; + if role0_key.len() != 32 { + anyhow::bail!( + "Role0 Public Key must have 32 bytes: {role0_hex}, len: {}", + role0_key.len() + ); + } + let role0 = role0_key.try_into().map_err(|e| { + anyhow::anyhow!( + "Unable to read Role0 Public Key, this should never happen. Eror: {e:?}" + ) + })?; + Ok(Role0PublicKey(role0)) + } +} + +impl Display for Role0PublicKey { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { + write!(f, "0x{}", hex::encode(self.0)) + } +} diff --git a/rust/signed_doc/src/signature/mod.rs b/rust/signed_doc/src/signature/mod.rs new file mode 100644 index 00000000000..77fc98fcb9b --- /dev/null +++ b/rust/signed_doc/src/signature/mod.rs @@ -0,0 +1,4 @@ +//! Catalyst Signed Document COSE Signature information. +mod kid; + +pub use kid::Kid; From 2a2aea13a4cca888432e749c70ec68c2383808ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Rosales?= Date: Sat, 4 Jan 2025 00:04:22 -0600 Subject: [PATCH 23/71] feat(rust/signed-doc): add types for Metadata fields. * add content_encoding and content_type fields to Metadata * update the example for making signed docs --- rust/signed_doc/examples/mk_signed_doc.rs | 14 +-- rust/signed_doc/src/lib.rs | 63 +++--------- .../src/metadata/content_encoding.rs | 32 +++++++ rust/signed_doc/src/metadata/content_type.rs | 24 +++++ rust/signed_doc/src/metadata/document_id.rs | 44 +++++++++ rust/signed_doc/src/metadata/document_type.rs | 39 ++++++++ .../src/metadata/document_version.rs | 38 ++++++++ rust/signed_doc/src/metadata/mod.rs | 96 +++++++++++++++---- .../src/metadata/uuid_type/uuid_v4.rs | 4 +- .../src/metadata/uuid_type/uuid_v7.rs | 13 ++- rust/signed_doc/src/signature/kid/mod.rs | 9 ++ 11 files changed, 296 insertions(+), 80 deletions(-) create mode 100644 rust/signed_doc/src/metadata/content_encoding.rs create mode 100644 rust/signed_doc/src/metadata/content_type.rs create mode 100644 rust/signed_doc/src/metadata/document_id.rs create mode 100644 rust/signed_doc/src/metadata/document_type.rs create mode 100644 rust/signed_doc/src/metadata/document_version.rs diff --git a/rust/signed_doc/examples/mk_signed_doc.rs b/rust/signed_doc/examples/mk_signed_doc.rs index 12cab376a9f..ad8a16365e0 100644 --- a/rust/signed_doc/examples/mk_signed_doc.rs +++ b/rust/signed_doc/examples/mk_signed_doc.rs @@ -6,7 +6,6 @@ use std::{ fs::{read_to_string, File}, io::{Read, Write}, path::PathBuf, - str::FromStr, }; use clap::Parser; @@ -58,7 +57,7 @@ enum Cli { }, } -const CONTENT_ENCODING_KEY: &str = "content encoding"; +const CONTENT_ENCODING_KEY: &str = "Content-Encoding"; const CONTENT_ENCODING_VALUE: &str = "br"; const UUID_CBOR_TAG: u64 = 37; @@ -120,7 +119,8 @@ impl Cli { } => { let doc_schema = load_schema_from_file(&schema)?; let json_doc = load_json_from_file(&doc)?; - let json_meta = load_json_from_file(&meta)?; + let json_meta = load_json_from_file(&meta) + .map_err(|e| anyhow::anyhow!("Failed to load metadata from file: {e}"))?; validate_json(&json_doc, &doc_schema)?; let compressed_doc = brotli_compress_json(&json_doc)?; let empty_cose_sign = build_empty_cose_doc(compressed_doc, &json_meta); @@ -299,14 +299,13 @@ fn validate_cose( validate_json(&json_doc, schema)?; for sign in &cose.signatures { - let key_id = sign.protected.header.key_id.clone(); + let key_id = &sign.protected.header.key_id; anyhow::ensure!( !key_id.is_empty(), "COSE missing signature protected header `kid` field " ); - let kid_str = String::from_utf8_lossy(&key_id); - let kid = Kid::from_str(&kid_str)?; + let kid = Kid::try_from(key_id.as_ref())?; println!("Signature Key ID: {kid}"); let data_to_sign = cose.tbs_data(&[], sign); let signature_bytes = sign.signature.as_slice().try_into().map_err(|_| { @@ -338,12 +337,13 @@ fn validate_cose_protected_header(cose: &coset::CoseSign) -> anyhow::Result<()> cose.protected.header.content_type == expected_header.content_type, "Invalid COSE document protected header `content-type` field" ); + println!("HEADER REST: \n{:?}", cose.protected.header.rest); 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" + "Invalid COSE document protected header" ); let Some((_, value)) = cose diff --git a/rust/signed_doc/src/lib.rs b/rust/signed_doc/src/lib.rs index 41ff63dda0e..6a5d8a60307 100644 --- a/rust/signed_doc/src/lib.rs +++ b/rust/signed_doc/src/lib.rs @@ -14,11 +14,6 @@ mod signature; pub use metadata::{DocumentRef, Metadata, UuidV7}; pub use signature::Kid; -/// 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"; - /// Keep all the contents private. /// Better even to use a structure like this. Wrapping in an Arc means we don't have to /// manage the Arc anywhere else. These are likely to be large, best to have the Arc be @@ -77,37 +72,23 @@ impl TryFrom> for CatalystSignedDocument { let cose = coset::CoseSign::from_tagged_slice(&cose_bytes) .or(coset::CoseSign::from_slice(&cose_bytes)) .map_err(|e| anyhow::anyhow!("Invalid COSE Sign document: {e}"))?; - let mut content_errors = Vec::new(); - let expected_header = cose_protected_header(); - if cose.protected.header.content_type != expected_header.content_type { - content_errors - .push("Invalid COSE document protected header `content-type` field".to_string()); - } + let mut content_errors = Vec::new(); - if !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()) - }) { - content_errors.push( - "Invalid COSE document protected header {CONTENT_ENCODING_KEY} field".to_string(), - ); - } let metadata = Metadata::from(&cose.protected); + if metadata.has_error() { content_errors.extend_from_slice(metadata.content_errors()); } - 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 payload = if let Some(payload) = &cose.payload { + let mut buf = Vec::new(); + let mut bytes = payload.as_slice(); + brotli::BrotliDecompress(&mut bytes, &mut buf)?; + serde_json::from_slice(&buf)? + } else { + println!("COSE missing payload field with the JSON content in it"); + serde_json::Value::Object(serde_json::Map::new()) }; let signatures = cose.signatures.clone(); let inner = InnerCatalystSignedDocument { @@ -173,27 +154,5 @@ impl CatalystSignedDocument { pub fn doc_section(&self) -> Option { self.inner.metadata.doc_section() } -} - -/// Generate the COSE protected header used by Catalyst Signed Document. -fn cose_protected_header() -> coset::Header { - coset::HeaderBuilder::new() - .content_format(coset::iana::CoapContentFormat::Json) - .text_value( - CONTENT_ENCODING_KEY.to_string(), - CONTENT_ENCODING_VALUE.to_string().into(), - ) - .build() -} -/// Find a value for a given key in the protected header. -fn cose_protected_header_find( - cose: &coset::CoseSign, rest_key: &str, -) -> Option { - cose.protected - .header - .rest - .iter() - .find(|(key, _)| key == &coset::Label::Text(rest_key.to_string())) - .map(|(_, value)| value.clone()) } diff --git a/rust/signed_doc/src/metadata/content_encoding.rs b/rust/signed_doc/src/metadata/content_encoding.rs new file mode 100644 index 00000000000..3309df87a37 --- /dev/null +++ b/rust/signed_doc/src/metadata/content_encoding.rs @@ -0,0 +1,32 @@ +//! Document Payload Content Encoding. + +/// Catalyst Signed Document Content Encoding Key. +const CONTENT_ENCODING_KEY: &str = "Content-Encoding"; + +/// IANA `CoAP` Content Encoding. +#[derive(Debug, serde::Deserialize)] +#[serde(untagged)] +pub enum ContentEncoding { + /// Brotli compression.format. + #[serde(rename = "br")] + Brotli, +} + +impl TryFrom<&coset::cbor::Value> for ContentEncoding { + type Error = anyhow::Error; + + #[allow(clippy::todo)] + fn try_from(val: &coset::cbor::Value) -> anyhow::Result { + match val.as_text() { + Some(encoding) => { + match encoding.to_string().to_lowercase().as_ref() { + "br" => Ok(ContentEncoding::Brotli), + _ => anyhow::bail!("Unsupported Content Encoding: {encoding}"), + } + }, + _ => { + anyhow::bail!("Expected Content Encoding to be a string"); + }, + } + } +} diff --git a/rust/signed_doc/src/metadata/content_type.rs b/rust/signed_doc/src/metadata/content_type.rs new file mode 100644 index 00000000000..782027741d3 --- /dev/null +++ b/rust/signed_doc/src/metadata/content_type.rs @@ -0,0 +1,24 @@ +//! Document Payload Content Type. + +/// Payload Content Type. +#[derive(Debug, serde::Deserialize)] +#[serde(untagged, rename_all_fields = "lowercase")] +pub enum ContentType { + /// 'application/cbor' + Cbor, + /// 'application/json' + Json, +} + +impl TryFrom<&coset::ContentType> for ContentType { + type Error = anyhow::Error; + + fn try_from(value: &coset::ContentType) -> Result { + use coset::iana::CoapContentFormat as Format; + match value { + coset::ContentType::Assigned(Format::Json) => Ok(ContentType::Json), + coset::ContentType::Assigned(Format::Cbor) => Ok(ContentType::Cbor), + _ => anyhow::bail!("Unsupported Content Type {value:?}"), + } + } +} diff --git a/rust/signed_doc/src/metadata/document_id.rs b/rust/signed_doc/src/metadata/document_id.rs new file mode 100644 index 00000000000..84180908222 --- /dev/null +++ b/rust/signed_doc/src/metadata/document_id.rs @@ -0,0 +1,44 @@ +//! Document ID. +use std::fmt::{Display, Formatter}; + +use super::UuidV7; + +/// Catalyst Document ID. +#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, serde::Deserialize)] +#[serde(from = "UuidV7")] +pub struct DocumentId { + /// Inner UUID type + uuid: UuidV7, +} + +impl DocumentId { + /// Generates a zeroed out `UUIDv7` that can never be valid. + pub fn invalid() -> Self { + Self { + uuid: UuidV7::invalid(), + } + } + + /// Check if this is a valid `UUIDv7`. + pub fn is_valid(&self) -> bool { + self.uuid.is_valid() + } + + /// Returns the `uuid::Uuid` type. + #[must_use] + pub fn uuid(&self) -> uuid::Uuid { + self.uuid.uuid() + } +} + +impl Display for DocumentId { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { + write!(f, "{}", self.uuid) + } +} + +impl From for DocumentId { + fn from(uuid: UuidV7) -> Self { + Self { uuid } + } +} diff --git a/rust/signed_doc/src/metadata/document_type.rs b/rust/signed_doc/src/metadata/document_type.rs new file mode 100644 index 00000000000..3aa0fddfe6c --- /dev/null +++ b/rust/signed_doc/src/metadata/document_type.rs @@ -0,0 +1,39 @@ +//! Document Type. +use std::fmt::{Display, Formatter}; + +use super::UuidV4; + +/// Catalyst Document Type. +#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, serde::Deserialize)] +#[serde(from = "UuidV4")] +pub struct DocumentType(UuidV4); + +impl DocumentType { + /// Generates a zeroed out `UUIDv4` that can never be valid. + pub fn invalid() -> Self { + Self(UuidV4::invalid()) + } + + /// Check if this is a valid `UUIDv4`. + pub fn is_valid(&self) -> bool { + self.0.is_valid() + } + + /// Returns the `uuid::Uuid` type. + #[must_use] + pub fn uuid(&self) -> uuid::Uuid { + self.0.uuid() + } +} + +impl Display for DocumentType { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { + write!(f, "{}", self.0) + } +} + +impl From for DocumentType { + fn from(value: UuidV4) -> Self { + Self(value) + } +} diff --git a/rust/signed_doc/src/metadata/document_version.rs b/rust/signed_doc/src/metadata/document_version.rs new file mode 100644 index 00000000000..21e44ccd8b3 --- /dev/null +++ b/rust/signed_doc/src/metadata/document_version.rs @@ -0,0 +1,38 @@ +//! Document Version. +use std::fmt::{Display, Formatter}; + +use super::UuidV7; + +/// Catalyst Document Version. +#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, serde::Deserialize)] +pub struct DocumentVersion(UuidV7); + +impl DocumentVersion { + /// Generates a zeroed out `UUIDv7` that can never be valid. + pub fn invalid() -> Self { + Self(UuidV7::invalid()) + } + + /// Check if this is a valid `UUIDv7`. + pub fn is_valid(&self) -> bool { + self.0.is_valid() + } + + /// Returns the `uuid::Uuid` type. + #[must_use] + pub fn uuid(&self) -> uuid::Uuid { + self.0.uuid() + } +} + +impl Display for DocumentVersion { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { + write!(f, "{}", self.0) + } +} + +impl From for DocumentVersion { + fn from(value: UuidV7) -> Self { + Self(value) + } +} diff --git a/rust/signed_doc/src/metadata/mod.rs b/rust/signed_doc/src/metadata/mod.rs index b9c10a2d33b..95789641767 100644 --- a/rust/signed_doc/src/metadata/mod.rs +++ b/rust/signed_doc/src/metadata/mod.rs @@ -1,29 +1,48 @@ //! Catalyst Signed Document Metadata. use std::fmt::{Display, Formatter}; +mod content_encoding; +mod content_type; +mod document_id; mod document_ref; +mod document_type; +mod document_version; mod uuid_type; +pub use content_encoding::ContentEncoding; +pub use content_type::ContentType; +pub use document_id::DocumentId; pub use document_ref::DocumentRef; +pub use document_type::DocumentType; +pub use document_version::DocumentVersion; pub use uuid_type::{UuidV4, UuidV7}; +/// Catalyst Signed Document Content Encoding Key. +const CONTENT_ENCODING_KEY: &str = "Content-Encoding"; + /// Document Metadata. #[derive(Debug, serde::Deserialize)] pub struct Metadata { /// Document Type `UUIDv4`. - r#type: UuidV4, + #[serde(rename = "type")] + doc_type: DocumentType, /// Document ID `UUIDv7`. - id: UuidV7, + id: DocumentId, /// Document Version `UUIDv7`. - ver: UuidV7, + ver: DocumentVersion, /// Reference to the latest document. - r#ref: Option, + #[serde(rename = "ref")] + doc_ref: Option, /// Reference to the document template. template: Option, /// Reference to the document reply. reply: Option, /// Reference to the document section. section: Option, + /// Document Payload Content Type. + content_type: Option, + /// Document Payload Content Encoding. + content_encoding: Option, /// Metadata Content Errors #[serde(skip)] content_errors: Vec, @@ -33,7 +52,7 @@ impl Metadata { /// Return Document Type `UUIDv4`. #[must_use] pub fn doc_type(&self) -> uuid::Uuid { - self.r#type.uuid() + self.doc_type.uuid() } /// Return Document ID `UUIDv7`. @@ -51,7 +70,7 @@ impl Metadata { /// Return Last Document Reference `Option`. #[must_use] pub fn doc_ref(&self) -> Option { - self.r#ref + self.doc_ref } /// Return Document Template `Option`. @@ -88,13 +107,15 @@ impl Metadata { 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_type: {},", self.doc_type)?; writeln!(f, " doc_id: {},", self.id)?; writeln!(f, " doc_ver: {},", self.ver)?; - writeln!(f, " doc_ref: {:?},", self.r#ref)?; + writeln!(f, " doc_ref: {:?},", self.doc_ref)?; writeln!(f, " doc_template: {:?},", self.template)?; writeln!(f, " doc_reply: {:?},", self.reply)?; writeln!(f, " doc_section: {:?}", self.section)?; + writeln!(f, " content_type: {:?}", self.content_type)?; + writeln!(f, " content_encoding: {:?}", self.content_encoding)?; writeln!(f, "}}") } } @@ -102,13 +123,15 @@ impl Display for Metadata { impl Default for Metadata { fn default() -> Self { Self { - r#type: UuidV4::invalid(), - id: UuidV7::invalid(), - ver: UuidV7::invalid(), - r#ref: None, + doc_type: DocumentType::invalid(), + id: DocumentId::invalid(), + ver: DocumentVersion::invalid(), + doc_ref: None, template: None, reply: None, section: None, + content_type: None, + content_encoding: None, content_errors: Vec::new(), } } @@ -131,11 +154,50 @@ impl From<&coset::ProtectedHeader> for Metadata { let mut metadata = Metadata::default(); let mut errors = Vec::new(); + match protected.header.content_type.as_ref() { + Some(iana_content_type) => { + match ContentType::try_from(iana_content_type) { + Ok(content_type) => metadata.content_type = Some(content_type), + Err(e) => { + errors.push(format!("Invalid Document Content-Type: {e}")); + }, + } + }, + None => { + errors.push( + "COSE document protected header `content-type` field is missing".to_string(), + ); + }, + } + match protected.header.rest.iter().find(|(key, _)| { + if let coset::Label::Text(label) = key { + label.eq_ignore_ascii_case(CONTENT_ENCODING_KEY) + } else { + false + } + }) { + Some((_key, value)) => { + match ContentEncoding::try_from(value) { + Ok(encoding) => { + metadata.content_encoding = Some(encoding); + }, + Err(e) => { + errors.push(format!("Invalid Document Content Encoding: {e}")); + }, + } + }, + _ => { + errors.push( + "Invalid COSE document protected header '{CONTENT_ENCODING_KEY}' label" + .to_string(), + ); + }, + } match cose_protected_header_find(protected, "type") { Some(doc_type) => { match UuidV4::try_from(&doc_type) { Ok(doc_type_uuid) => { - metadata.r#type = doc_type_uuid; + metadata.doc_type = doc_type_uuid.into(); }, Err(e) => { errors.push(format!("Document `type` is invalid: {e}")); @@ -149,7 +211,7 @@ impl From<&coset::ProtectedHeader> for Metadata { Some(doc_id) => { match UuidV7::try_from(&doc_id) { Ok(doc_id_uuid) => { - metadata.id = doc_id_uuid; + metadata.id = doc_id_uuid.into(); }, Err(e) => { errors.push(format!("Document `id` is invalid: {e}")); @@ -163,12 +225,12 @@ impl From<&coset::ProtectedHeader> for Metadata { Some(doc_ver) => { match UuidV7::try_from(&doc_ver) { Ok(doc_ver_uuid) => { - if doc_ver_uuid < metadata.id { + if doc_ver_uuid.uuid() < metadata.id.uuid() { errors.push(format!( "Document Version {doc_ver_uuid} cannot be smaller than Document ID {}", metadata.id )); } else { - metadata.ver = doc_ver_uuid; + metadata.ver = doc_ver_uuid.into(); } }, Err(e) => { @@ -184,7 +246,7 @@ impl From<&coset::ProtectedHeader> for Metadata { if let Some(cbor_doc_ref) = cose_protected_header_find(protected, "ref") { match DocumentRef::try_from(&cbor_doc_ref) { Ok(doc_ref) => { - metadata.r#ref = Some(doc_ref); + metadata.doc_ref = Some(doc_ref); }, Err(e) => { errors.push(format!( diff --git a/rust/signed_doc/src/metadata/uuid_type/uuid_v4.rs b/rust/signed_doc/src/metadata/uuid_type/uuid_v4.rs index 95084221076..fba37b699d2 100644 --- a/rust/signed_doc/src/metadata/uuid_type/uuid_v4.rs +++ b/rust/signed_doc/src/metadata/uuid_type/uuid_v4.rs @@ -4,8 +4,8 @@ use std::fmt::{Display, Formatter}; use super::{decode_cbor_uuid, INVALID_UUID}; /// Type representing a `UUIDv4`. -#[derive(Copy, Clone, Debug, serde::Deserialize)] -#[serde(transparent)] +#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, serde::Deserialize)] +#[serde(from = "uuid::Uuid")] pub struct UuidV4 { /// UUID uuid: uuid::Uuid, diff --git a/rust/signed_doc/src/metadata/uuid_type/uuid_v7.rs b/rust/signed_doc/src/metadata/uuid_type/uuid_v7.rs index bd1a2a00bcb..7ef7a3a2446 100644 --- a/rust/signed_doc/src/metadata/uuid_type/uuid_v7.rs +++ b/rust/signed_doc/src/metadata/uuid_type/uuid_v7.rs @@ -4,8 +4,8 @@ use std::fmt::{Display, Formatter}; use super::{decode_cbor_uuid, INVALID_UUID}; /// Type representing a `UUIDv7`. -#[derive(Copy, Clone, Debug, serde::Deserialize, PartialEq, PartialOrd)] -#[serde(transparent)] +#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, serde::Deserialize)] +#[serde(from = "uuid::Uuid")] pub struct UuidV7 { /// UUID uuid: uuid::Uuid, @@ -58,3 +58,12 @@ impl TryFrom<&coset::cbor::Value> for UuidV7 { } } } + +/// Returns a `UUIDv7` from `uuid::Uuid`. +/// +/// NOTE: This does not guarantee that the `UUID` is valid. +impl From for UuidV7 { + fn from(uuid: uuid::Uuid) -> Self { + Self { uuid } + } +} diff --git a/rust/signed_doc/src/signature/kid/mod.rs b/rust/signed_doc/src/signature/kid/mod.rs index e0984a1586b..dcb041af552 100644 --- a/rust/signed_doc/src/signature/kid/mod.rs +++ b/rust/signed_doc/src/signature/kid/mod.rs @@ -91,6 +91,15 @@ impl Display for Kid { } } +impl TryFrom<&[u8]> for Kid { + type Error = anyhow::Error; + + fn try_from(value: &[u8]) -> Result { + let kid_str = String::from_utf8_lossy(value); + Kid::from_str(&kid_str) + } +} + #[cfg(test)] mod tests { use std::str::FromStr; From 3f77f5ecb00427023e1571b7fc512f6d6c4f8f26 Mon Sep 17 00:00:00 2001 From: Steven Johnson Date: Sun, 5 Jan 2025 12:22:40 +0700 Subject: [PATCH 24/71] docs(docs): Add formal specification for the RBAC KID URL format. --- .config/dictionaries/project.dic | 3 +- .../08_concepts/rbac_kid_uri/.pages | 3 + .../08_concepts/rbac_kid_uri/kiduri.md | 180 ++++++++++++++++++ 3 files changed, 185 insertions(+), 1 deletion(-) create mode 100644 docs/src/architecture/08_concepts/rbac_kid_uri/.pages create mode 100644 docs/src/architecture/08_concepts/rbac_kid_uri/kiduri.md diff --git a/.config/dictionaries/project.dic b/.config/dictionaries/project.dic index d3c705af86f..0b06420e656 100644 --- a/.config/dictionaries/project.dic +++ b/.config/dictionaries/project.dic @@ -260,10 +260,11 @@ unlinkat upnp ureq userid +userinfo utimensat +UTXO uuidv4 uuidv7 -UTXO vitss Vkey vkeywitness diff --git a/docs/src/architecture/08_concepts/rbac_kid_uri/.pages b/docs/src/architecture/08_concepts/rbac_kid_uri/.pages new file mode 100644 index 00000000000..0bcc138749f --- /dev/null +++ b/docs/src/architecture/08_concepts/rbac_kid_uri/.pages @@ -0,0 +1,3 @@ +title: RBAC KID (Key Identifier) URI +arrange: + - kiduri.md diff --git a/docs/src/architecture/08_concepts/rbac_kid_uri/kiduri.md b/docs/src/architecture/08_concepts/rbac_kid_uri/kiduri.md new file mode 100644 index 00000000000..14deb9e92b5 --- /dev/null +++ b/docs/src/architecture/08_concepts/rbac_kid_uri/kiduri.md @@ -0,0 +1,180 @@ +--- +Title: RBAC Key Identifier URI Specification +Category: Catalyst +Status: Proposed +Authors: + - Steven Johnson +Implementors: + - Catalyst Fund 14 +Discussions: [] +Created: 2025-01-05 +License: CC-BY-4.0 +--- + +* [Abstract](#abstract) +* [Motivation: why is this CIP necessary?](#motivation-why-is-this-cip-necessary) +* [Specification](#specification) + * [URI](#uri) + * [`scheme`](#scheme) + * [`authority`](#authority) + * [`authority` - `host`](#authority---host) + * [List of defined hosts](#list-of-defined-hosts) + * [`authority` - `userinfo`](#authority---userinfo) + * [Lists of defined subnetwork `userinfo` values](#lists-of-defined-subnetwork-userinfo-values) + * [Cardano](#cardano) + * [`path`](#path) +* [Reference Implementation](#reference-implementation) +* [Test Vectors](#test-vectors) +* [Rationale: how does this CIP achieve its goals?](#rationale-how-does-this-cip-achieve-its-goals) +* [Path to Active](#path-to-active) + * [Acceptance Criteria](#acceptance-criteria) + * [Implementation Plan](#implementation-plan) +* [Copyright](#copyright) + +## Abstract + +Definition of a [URI] which allows for RBAC keys used for different purposes to be easily and +unambiguously identified. + +## Motivation: why is this CIP necessary? + +There is a need to identify which Key from a RBAC registration was used to sign data. +RBAC defines a universal keychain of different keys that can be used for different purposes. +They can be used not only for Signatures, but also Encryption. + +Therefore, there needs to be an unambiguous and easy to lookup identifier to signify which key was +used for a particular purpose. + +This document defines a [URI] scheme to unambiguously define a particular key with reference to a +particular RBAC keychain. + +## Specification + +### URI + +The RBAC Kid is formatted using a [Universal Resource Identifier]. +Refer to [RFC3986] for the specification of the URI format. + +### `scheme` + +The [scheme](https://datatracker.ietf.org/doc/html/rfc3986#section-3.1) **MUST** be `kid.catalyst-rbac`; + +### `authority` + +The [authority](https://datatracker.ietf.org/doc/html/rfc3986#section-3.2) references the blockchain or network +the key was registered within. + +It is perfectly valid for a Kid to reference a different network than the place where the Key is used. +For example, a `cardano` KID can be used to post documents to `IPFS`. +Its purpose is to define WHERE the key was registered, and nothing more. + +The Authority will consist of a `host` and optional `userinfo`. + +#### `authority` - `host` + +The [host](https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2) +refers to the network type where the RBAC registration was made. +It **IS NOT** resolvable with **DNS**, and **IS NOT** a public host name. +It is used as a decentralized network identifier. +The consumer of the `KID` must be able to resolve these host names. + +##### List of defined hosts + +| `host` | Description | +| --- | --- | +| `cardano` | Cardano Blockchain | +| `midnight` | Midnight Blockchain | +| `ethereum` | Ethereum Blockchain | +| `cosmos` | Cosmos Blockchain | + +#### `authority` - `userinfo` + +The [userinfo](https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.1) +is used to distinguish a subnetwork from the primary main network. +The absence of `userinfo` is used to indicate the primary main network. + +##### Lists of defined subnetwork `userinfo` values + +###### Cardano + +| `userinfo` | Description | +| --- | --- | +| `preprod` | Cardano Pre-Production Network | +| `preview` | Cardano Preview Network | +| 0x | Cardano network identified by this magic number in hex | + +### `path` + +The [path](https://datatracker.ietf.org/doc/html/rfc3986#section-3.3) defines the actual key within the registration. +Keys are defined relative to the very first Role0 Key registered in any RBAC registration. + +The overall `path` specification is: `//#encrypt` + +* `` - This is the very first role 0 key used to post the registration to the network. + * It is the [Base64 URL] encoded binary data of the role 0 public key. + * This does not change, even if the Initial Role 0 key is revoked. + * This allows for an unambiguous identifier for the RBAC keychain. + * It is not necessarily the key being identified. +* `` - This is the Role number being used. + * It is a positive number, starting at 0, and no greater than 65535. +* `` - This is the rotation of the defined role key being identified. + * It starts at 0 for the first published key for the role, and increments by one for each subsequent published rotation. + * This number refers to the published sequence of keys for the role in the RBAC registration keychain, + not the index used in the key derivation. + * It is positive and no greater than 65535. +* `#encrypt` - [Fragment](https://datatracker.ietf.org/doc/html/rfc3986#section-3.5) + disambiguates Encryption Public Keys from signing public keys. + * Roles can have 1 active public signing key, and 1 active public encryption key. + * By default, the URL is referencing the signing public key. + * If a public encryption key is being identified, then the fragment `#encrypt` is appended to the [Universal Resource Identifier]. + +## Reference Implementation + +The first implementation will be Catalyst Voices. + +## Test Vectors + +* `kid.catalyst-rbac://cardano//0/0` + * A Signing key registered on the Cardano Main network. + * Role 0 - Rotation 0. + In this example, it is exactly the same as the ``. +* `kid.catalyst-rbac://preprod@cardano/ed25519//7/3` + * A Signing key registered on the Cardano pre-production network. + * Role 7 - Rotation 3. + The Key for Role 7, and its third published rotation + (i.e., the fourth key published, the first is the initial key, plus 3 rotations following it). +* `kid.catalyst-rbac://preprod@cardano/ed25519//2/0#encrypt` + * A Public Encryption key registered on the Cardano pre-production network. + * Role 2 - Rotation 0. + The initially published Public Encryption Key for Role 2. +* `kid.catalyst-rbac://midnight//0/1` + * A Signing key registered on the Midnight Blockchain Main network + * Role 0 - Rotation 1. + In this example, it is NOT the same as the ``, as it identifies the first rotation after ``. +* `kid.catalyst-rbac://midnight/encrypt//2/1#encrypt` + * A public encryption key registered on the Midnight Blockchain Main network. + * Role 2 - Rotation 1. + +## Rationale: how does this CIP achieve its goals? + +By creating a [URI] to identify keys, +we allow the unambiguous and flexible identification of any RBAC Key that was used for any purpose. + +## Path to Active + +### Acceptance Criteria + +Working Implementation before Fund 14. + +### Implementation Plan + +Fund 14 project catalyst will deploy this scheme for Key Identification. + +## Copyright + +This document is licensed under [CC-BY-4.0](https://creativecommons.org/licenses/by/4.0/legalcode). + +[URI]: https://datatracker.ietf.org/doc/html/rfc3986 +[Universal Resource Identifier]: https://datatracker.ietf.org/doc/html/rfc3986 +[RFC3986]: https://datatracker.ietf.org/doc/html/rfc3986 +[Base64 URL]: https://datatracker.ietf.org/doc/html/rfc4648#section-5 From 3dfe9cac6ef58e7b199b7cf9de2573bed2574bfe Mon Sep 17 00:00:00 2001 From: Steven Johnson Date: Sun, 5 Jan 2025 12:50:44 +0700 Subject: [PATCH 25/71] fix(docs): Fix the example Kid URI --- docs/src/architecture/08_concepts/rbac_kid_uri/kiduri.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/src/architecture/08_concepts/rbac_kid_uri/kiduri.md b/docs/src/architecture/08_concepts/rbac_kid_uri/kiduri.md index 14deb9e92b5..fd09c684705 100644 --- a/docs/src/architecture/08_concepts/rbac_kid_uri/kiduri.md +++ b/docs/src/architecture/08_concepts/rbac_kid_uri/kiduri.md @@ -138,12 +138,12 @@ The first implementation will be Catalyst Voices. * A Signing key registered on the Cardano Main network. * Role 0 - Rotation 0. In this example, it is exactly the same as the ``. -* `kid.catalyst-rbac://preprod@cardano/ed25519//7/3` +* `kid.catalyst-rbac://preprod@cardano//7/3` * A Signing key registered on the Cardano pre-production network. * Role 7 - Rotation 3. The Key for Role 7, and its third published rotation (i.e., the fourth key published, the first is the initial key, plus 3 rotations following it). -* `kid.catalyst-rbac://preprod@cardano/ed25519//2/0#encrypt` +* `kid.catalyst-rbac://preprod@cardano//2/0#encrypt` * A Public Encryption key registered on the Cardano pre-production network. * Role 2 - Rotation 0. The initially published Public Encryption Key for Role 2. @@ -151,7 +151,7 @@ The first implementation will be Catalyst Voices. * A Signing key registered on the Midnight Blockchain Main network * Role 0 - Rotation 1. In this example, it is NOT the same as the ``, as it identifies the first rotation after ``. -* `kid.catalyst-rbac://midnight/encrypt//2/1#encrypt` +* `kid.catalyst-rbac://midnight//2/1#encrypt` * A public encryption key registered on the Midnight Blockchain Main network. * Role 2 - Rotation 1. From 642b8defa372618ff544a6035eda906ed98ee145 Mon Sep 17 00:00:00 2001 From: Steven Johnson Date: Mon, 6 Jan 2025 05:49:56 +0700 Subject: [PATCH 26/71] feat(rust): Update the KidURI struct to match the formal spec (#139) --- .config/dictionaries/project.dic | 3 + rust/signed_doc/Cargo.toml | 7 +- rust/signed_doc/examples/mk_signed_doc.rs | 4 +- rust/signed_doc/src/lib.rs | 3 +- rust/signed_doc/src/signature/kid/errors.rs | 43 ++++ .../src/signature/kid/key_rotation.rs | 42 ++++ .../src/signature/kid/key_version.rs | 19 -- rust/signed_doc/src/signature/kid/mod.rs | 236 +++++++++++++----- rust/signed_doc/src/signature/kid/role.rs | 42 ---- .../src/signature/kid/role_index.rs | 44 ++++ rust/signed_doc/src/signature/mod.rs | 2 +- 11 files changed, 314 insertions(+), 131 deletions(-) create mode 100644 rust/signed_doc/src/signature/kid/errors.rs create mode 100644 rust/signed_doc/src/signature/kid/key_rotation.rs delete mode 100644 rust/signed_doc/src/signature/kid/key_version.rs delete mode 100644 rust/signed_doc/src/signature/kid/role.rs create mode 100644 rust/signed_doc/src/signature/kid/role_index.rs diff --git a/.config/dictionaries/project.dic b/.config/dictionaries/project.dic index 0b06420e656..1453c623046 100644 --- a/.config/dictionaries/project.dic +++ b/.config/dictionaries/project.dic @@ -49,6 +49,7 @@ coverallsapp cpus crontabs crontagged +csprng cstring dalek dashmap @@ -208,6 +209,8 @@ reqwest retriggering ristretto rlib +rngs +rsplit rulelist RULENAME runable diff --git a/rust/signed_doc/Cargo.toml b/rust/signed_doc/Cargo.toml index b4e47d03137..cf5dc6e17bf 100644 --- a/rust/signed_doc/Cargo.toml +++ b/rust/signed_doc/Cargo.toml @@ -11,6 +11,7 @@ license.workspace = true workspace = true [dependencies] +cardano-blockchain-types = { version = "0.0.1", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "v0.0.11" } anyhow = "1.0.95" serde = { version = "1.0.217", features = ["derive"] } serde_json = "1.0.134" @@ -18,9 +19,13 @@ serde_json = "1.0.134" jsonschema = "0.18.3" coset = "0.3.8" brotli = "7.0.0" -ed25519-dalek = { version = "2.1.1", features = ["pem"] } +ed25519-dalek = { version = "2.1.1", features = ["pem", "rand_core"] } uuid = { version = "1.11.0", features = ["v4", "v7", "serde"] } hex = "0.4.3" +fluent-uri = "0.3.2" +thiserror = "2.0.9" +base64-url = "3.0.0" [dev-dependencies] clap = { version = "4.5.23", features = ["derive", "env"] } +rand = "0.8.5" \ No newline at end of file diff --git a/rust/signed_doc/examples/mk_signed_doc.rs b/rust/signed_doc/examples/mk_signed_doc.rs index ad8a16365e0..21f34f88bd2 100644 --- a/rust/signed_doc/examples/mk_signed_doc.rs +++ b/rust/signed_doc/examples/mk_signed_doc.rs @@ -14,7 +14,7 @@ use ed25519_dalek::{ ed25519::signature::Signer, pkcs8::{DecodePrivateKey, DecodePublicKey}, }; -use signed_doc::{DocumentRef, Kid, Metadata, UuidV7}; +use signed_doc::{DocumentRef, KidURI, Metadata, UuidV7}; fn main() { if let Err(err) = Cli::parse().exec() { @@ -305,7 +305,7 @@ fn validate_cose( "COSE missing signature protected header `kid` field " ); - let kid = Kid::try_from(key_id.as_ref())?; + let kid = KidURI::try_from(key_id.as_ref())?; println!("Signature Key ID: {kid}"); let data_to_sign = cose.tbs_data(&[], sign); let signature_bytes = sign.signature.as_slice().try_into().map_err(|_| { diff --git a/rust/signed_doc/src/lib.rs b/rust/signed_doc/src/lib.rs index 6a5d8a60307..01f13543fa9 100644 --- a/rust/signed_doc/src/lib.rs +++ b/rust/signed_doc/src/lib.rs @@ -12,7 +12,7 @@ mod metadata; mod signature; pub use metadata::{DocumentRef, Metadata, UuidV7}; -pub use signature::Kid; +pub use signature::KidURI; /// Keep all the contents private. /// Better even to use a structure like this. Wrapping in an Arc means we don't have to @@ -154,5 +154,4 @@ impl CatalystSignedDocument { pub fn doc_section(&self) -> Option { self.inner.metadata.doc_section() } - } diff --git a/rust/signed_doc/src/signature/kid/errors.rs b/rust/signed_doc/src/signature/kid/errors.rs new file mode 100644 index 00000000000..f7aa25f230e --- /dev/null +++ b/rust/signed_doc/src/signature/kid/errors.rs @@ -0,0 +1,43 @@ +//! Errors returned by this type + +use thiserror::Error; + +use super::{key_rotation::KeyRotationError, role_index::RoleIndexError}; + +/// Errors that can occur when parsing a `KidURI` +#[derive(Error, Debug)] +pub enum KidURIError { + /// Invalid KID URI + #[error("Invalid URI")] + InvalidURI(#[from] fluent_uri::error::ParseError), + /// Invalid Scheme, not a KID URI + #[error("Invalid Scheme, not a KID URI")] + InvalidScheme, + /// Network not defined in URI + #[error("No defined Network")] + NoDefinedNetwork, + /// Path of URI is invalid + #[error("Invalid Path")] + InvalidPath, + /// Role 0 Key in path is invalid + #[error("Invalid Role 0 Key")] + InvalidRole0Key, + /// Role 0 Key in path is not encoded correctly + #[error("Invalid Role 0 Key Encoding")] + InvalidRole0KeyEncoding(#[from] base64_url::base64::DecodeError), + /// Role Index is invalid + #[error("Invalid Role")] + InvalidRole, + /// Role Index is not encoded correctly + #[error("Invalid Role Index")] + InvalidRoleIndex(#[from] RoleIndexError), + /// Role Key Rotation is invalid + #[error("Invalid Rotation")] + InvalidRotation, + /// Role Key Rotation is not encoded correctly + #[error("Invalid Rotation Value")] + InvalidRotationValue(#[from] KeyRotationError), + /// Encryption key Identifier Fragment is not valid + #[error("Invalid Encryption Key Fragment")] + InvalidEncryptionKeyFragment, +} diff --git a/rust/signed_doc/src/signature/kid/key_rotation.rs b/rust/signed_doc/src/signature/kid/key_rotation.rs new file mode 100644 index 00000000000..e41fcb046e1 --- /dev/null +++ b/rust/signed_doc/src/signature/kid/key_rotation.rs @@ -0,0 +1,42 @@ +//! COSE Signature Protected Header `kid` Role0 Key Version. + +use std::{ + fmt::{Display, Formatter}, + num::ParseIntError, + str::FromStr, +}; + +use thiserror::Error; + +/// Errors from parsing the `KeyRotation` +#[derive(Error, Debug)] +#[allow(clippy::module_name_repetitions)] +pub enum KeyRotationError { + /// Key Rotation could not be parsed from a string + #[error("Invalid Role Key Rotation")] + InvalidRole(#[from] ParseIntError), +} + +/// Rotation count of the Role Key. +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct KeyRotation(u16); + +impl From for KeyRotation { + fn from(value: u16) -> Self { + Self(value) + } +} + +impl FromStr for KeyRotation { + type Err = KeyRotationError; + + fn from_str(s: &str) -> Result { + Ok(Self(s.parse::()?)) + } +} + +impl Display for KeyRotation { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { + write!(f, "{}", self.0) + } +} diff --git a/rust/signed_doc/src/signature/kid/key_version.rs b/rust/signed_doc/src/signature/kid/key_version.rs deleted file mode 100644 index 7314af0b04d..00000000000 --- a/rust/signed_doc/src/signature/kid/key_version.rs +++ /dev/null @@ -1,19 +0,0 @@ -//! COSE Signature Protected Header `kid` Role0 Key Version. - -use std::fmt::{Display, Formatter}; - -/// Version of the Role0 Key. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct KeyVersion(u16); - -impl From for KeyVersion { - fn from(value: u16) -> Self { - Self(value) - } -} - -impl Display for KeyVersion { - fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { - write!(f, "{}", self.0) - } -} diff --git a/rust/signed_doc/src/signature/kid/mod.rs b/rust/signed_doc/src/signature/kid/mod.rs index dcb041af552..71cc9380789 100644 --- a/rust/signed_doc/src/signature/kid/mod.rs +++ b/rust/signed_doc/src/signature/kid/mod.rs @@ -1,123 +1,231 @@ //! COSE Signature Protected Header `kid`. mod authority; -mod key_version; -mod role; +mod errors; +mod key_rotation; mod role0_pk; +mod role_index; use std::{ fmt::{Display, Formatter}, str::FromStr, }; -use authority::Authority; -use key_version::KeyVersion; -use role::Role; -use role0_pk::Role0PublicKey; +use ed25519_dalek::VerifyingKey; +use fluent_uri::{ + component::Scheme, + encoding::{ + encoder::{Fragment, Path}, + EStr, + }, + Uri, +}; +use key_rotation::KeyRotation; +use role_index::RoleIndex; /// Catalyst Signed Document Key ID /// /// Key ID associated with a `COSE` Signature that is structured as a Universal Resource /// Identifier (`URI`). #[derive(Debug, Clone)] -pub struct Kid { - /// URI Authority - authority: Authority, +#[allow(clippy::module_name_repetitions)] +pub struct KidURI { + /// Network + network: String, + /// Sub Network + subnet: Option, /// Role0 Public Key. - role0_public_key: Role0PublicKey, + role0_pk: VerifyingKey, /// User Role specified for the current document. - role: Role, - /// Role0 Public Key Version - key_version: KeyVersion, + role: RoleIndex, + /// Role Key Rotation count + rotation: KeyRotation, + /// Is this an Encryption Key + encryption: bool, } -impl Kid { +impl KidURI { + /// Encryption Key Identifier Fragment + const ENCRYPTION_FRAGMENT: &EStr = EStr::new_or_panic("encrypt"); /// URI scheme for Catalyst - const URI_SCHEME_PREFIX: &str = "catalyst_kid://"; + const SCHEME: &Scheme = Scheme::new_or_panic("kid.catalyst-rbac"); + + /// Get the network the `KidURI` is referencing the registration to. + #[must_use] + pub fn network(&self) -> (String, Option) { + (self.network.clone(), self.subnet.clone()) + } + + /// Is the key a signature type key. + #[must_use] + pub fn is_signature_key(&self) -> bool { + !self.encryption + } + + /// Is the key an encryption type key. + #[must_use] + pub fn is_encryption_key(&self) -> bool { + self.encryption + } + + /// Get the Initial Role 0 Key of the registration + #[must_use] + pub fn role0_pk(&self) -> VerifyingKey { + self.role0_pk + } + + /// Get the role index and its rotation count + #[must_use] + pub fn role_and_rotation(&self) -> (RoleIndex, KeyRotation) { + (self.role, self.rotation) + } } -impl FromStr for Kid { - type Err = anyhow::Error; +impl KidURI { + /// Create a new `KidURI` for a Signing Key + fn new( + network: &str, subnet: Option<&str>, role0_pk: VerifyingKey, role: RoleIndex, + rotation: KeyRotation, + ) -> Self { + Self { + network: network.to_string(), + subnet: subnet.map(str::to_string), + role0_pk, + role, + rotation, + encryption: false, + } + } - fn from_str(s: &str) -> anyhow::Result { - let Some(uri) = s.strip_prefix(Self::URI_SCHEME_PREFIX) else { - anyhow::bail!("Key ID scheme must be '{}': {s}", Self::URI_SCHEME_PREFIX); - }; + /// Create a new `KidURI` for an Encryption Key + fn new_encryption( + network: &str, subnet: Option<&str>, role0_pk: VerifyingKey, role: RoleIndex, + rotation: KeyRotation, + ) -> Self { + let mut kid = Self::new(network, subnet, role0_pk, role, rotation); + kid.encryption = true; + kid + } +} - let Some((authority_str, key_role_version)) = uri.split_once('/') else { - anyhow::bail!("Key ID must have an authority: {uri}"); - }; +impl FromStr for KidURI { + type Err = errors::KidURIError; - let authority = Authority::from_str(authority_str) - .map_err(|e| anyhow::anyhow!("Invalid Authority: {authority_str}. {e}"))?; + fn from_str(s: &str) -> Result { + let uri = Uri::parse(s)?; - let Some((role0_key_str, role_version)) = key_role_version.split_once('/') else { - anyhow::bail!("Expected Key ID have an Role0 Key set: {key_role_version}"); - }; + // Check if its the correct scheme. + if uri.scheme() != KidURI::SCHEME { + return Err(errors::KidURIError::InvalidScheme); + } - let role0_public_key = Role0PublicKey::from_str(role0_key_str) - .map_err(|e| anyhow::anyhow!("Invalid Role0 Public Key: {role0_key_str}. {e}"))?; + // Decode the network and subnet + let auth = uri + .authority() + .ok_or(errors::KidURIError::NoDefinedNetwork)?; + let network = auth.host(); + let subnet = auth.userinfo().map(std::string::ToString::to_string); - let Some((role_str, key_version_str)) = role_version.split_once('/') else { - anyhow::bail!("Expected Key ID have a role set"); - }; + let path: Vec<&EStr> = uri.path().split('/').collect(); - let role = Role::from_str(role_str) - .map_err(|e| anyhow::anyhow!("Invalid Role: {role_str}. {e}"))?; + // Can ONLY have 3 path components, no more and no less + // Less than 3 handled by errors below (4 because of leading `/` in path). + if path.len() > 4 { + return Err(errors::KidURIError::InvalidPath); + }; - let key_version: KeyVersion = u16::from_str(key_version_str) - .map_err(|e| anyhow::anyhow!("Invalid Key Version: {key_version_str}. {e}"))? - .into(); + // Decode and validate the Role0 Public key from the path + let encoded_role0_key = path.get(1).ok_or(errors::KidURIError::InvalidRole0Key)?; + let decoded_role0_key = + base64_url::decode(encoded_role0_key.decode().into_string_lossy().as_ref())?; + let role0_pk = cardano_blockchain_types::conversion::vkey_from_bytes(&decoded_role0_key) + .or(Err(errors::KidURIError::InvalidRole0Key))?; + + // Decode and validate the Role Index from the path. + let encoded_role_index = path.get(2).ok_or(errors::KidURIError::InvalidRole)?; + let decoded_role_index = encoded_role_index.decode().into_string_lossy(); + let role_index = decoded_role_index.parse::()?; + + // Decode and validate the Rotation Value from the path. + let encoded_rotation = path.get(3).ok_or(errors::KidURIError::InvalidRotation)?; + let decoded_rotation = encoded_rotation.decode().into_string_lossy(); + let rotation = decoded_rotation.parse::()?; + + let kid = { + if uri.has_fragment() { + if uri.fragment() == Some(Self::ENCRYPTION_FRAGMENT) { + Self::new_encryption(network, subnet.as_deref(), role0_pk, role_index, rotation) + } else { + return Err(errors::KidURIError::InvalidEncryptionKeyFragment); + } + } else { + Self::new(network, subnet.as_deref(), role0_pk, role_index, rotation) + } + }; - Ok(Kid { - authority, - role0_public_key, - role, - key_version, - }) + Ok(kid) } } -impl Display for Kid { +impl Display for KidURI { fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { + write!(f, "{}://", Self::SCHEME.as_str())?; + if let Some(subnet) = &self.subnet { + write!(f, "{subnet}@")?; + } write!( f, - "{}{}/{}/{}/{}", - Self::URI_SCHEME_PREFIX, - self.authority, - self.role0_public_key, + "{}/{}/{}/{}", + self.network, + base64_url::encode(self.role0_pk.as_bytes()), self.role, - self.key_version, - ) + self.rotation + )?; + if self.encryption { + write!(f, "#{}", Self::ENCRYPTION_FRAGMENT)?; + } + Ok(()) } } -impl TryFrom<&[u8]> for Kid { - type Error = anyhow::Error; +impl TryFrom<&[u8]> for KidURI { + type Error = errors::KidURIError; fn try_from(value: &[u8]) -> Result { let kid_str = String::from_utf8_lossy(value); - Kid::from_str(&kid_str) + KidURI::from_str(&kid_str) } } #[cfg(test)] mod tests { - use std::str::FromStr; + use ed25519_dalek::SigningKey; + use rand::rngs::OsRng; - use super::Kid; + use super::KidURI; - const KID_STR: &str = "catalyst_kid://cardano/0x0063ce08eccfdd5c93dd5cc9ca959fe669fd762fa816d70438efa90c0a75288c/3/0"; + const KID_TEST_VECTOR: [&str; 5] = [ + "kid.catalyst-rbac://cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE/0/0", + "kid.catalyst-rbac://preprod@cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE/7/3", + "kid.catalyst-rbac://preprod@cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE/2/0#encrypt", + "kid.catalyst-rbac://midnight/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE/0/1", + "kid.catalyst-rbac://midnight/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE/2/1#encrypt" + ]; #[test] fn test_kid_uri_from_str() { - let kid_str = KID_STR; - assert!(Kid::from_str(kid_str).is_ok()); + for kid_string in KID_TEST_VECTOR { + let kid = kid_string.parse::().unwrap(); + assert_eq!(format!("{kid}"), kid_string); + } } + #[ignore] #[test] - fn test_kid_uri_from_str_and_back() { - let kid_str = KID_STR; - let kid = Kid::from_str(kid_str).unwrap(); - assert_eq!(KID_STR, format!("{kid}")); + fn gen_pk() { + let mut csprng = OsRng; + let signing_key: SigningKey = SigningKey::generate(&mut csprng); + let vk = signing_key.verifying_key(); + let encoded_vk = base64_url::encode(vk.as_bytes()); + assert_eq!(encoded_vk, "1234"); } } diff --git a/rust/signed_doc/src/signature/kid/role.rs b/rust/signed_doc/src/signature/kid/role.rs deleted file mode 100644 index b69b6552803..00000000000 --- a/rust/signed_doc/src/signature/kid/role.rs +++ /dev/null @@ -1,42 +0,0 @@ -//! COSE Signature Protected Header `kid` URI Catalyst User Role. - -use std::{ - fmt::{Display, Formatter}, - str::FromStr, -}; - -/// Project Catalyst User Role associated with the signature. -/// -/// -#[repr(u16)] -#[derive(Debug, Copy, Clone)] -pub enum Role { - /// Voter = 0 - Zero, - /// Delegated Representative = 1 - One, - /// Voter Delegation = 2 - Two, - /// Proposer = 3 - Three, -} - -impl FromStr for Role { - type Err = anyhow::Error; - - fn from_str(s: &str) -> Result { - match s { - "0" => Ok(Role::Zero), - "1" => Ok(Role::One), - "2" => Ok(Role::Two), - "3" => Ok(Role::Three), - _ => Err(anyhow::anyhow!("Unknown Role: {}", s)), - } - } -} - -impl Display for Role { - fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { - write!(f, "{}", *self as u16) - } -} diff --git a/rust/signed_doc/src/signature/kid/role_index.rs b/rust/signed_doc/src/signature/kid/role_index.rs new file mode 100644 index 00000000000..ddb2b2cc4e0 --- /dev/null +++ b/rust/signed_doc/src/signature/kid/role_index.rs @@ -0,0 +1,44 @@ +//! COSE Signature Protected Header `kid` URI Catalyst User Role. + +use std::{ + fmt::{Display, Formatter}, + num::ParseIntError, + str::FromStr, +}; + +use thiserror::Error; + +/// Role Index parsing error +#[derive(Error, Debug)] +#[allow(clippy::module_name_repetitions)] +pub enum RoleIndexError { + /// Failed to parse the role index + #[error("Invalid Role Index")] + InvalidRole(#[from] ParseIntError), +} + +/// Project Catalyst User Role Index. +/// +/// +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct RoleIndex(u16); + +impl From for RoleIndex { + fn from(value: u16) -> Self { + Self(value) + } +} + +impl FromStr for RoleIndex { + type Err = RoleIndexError; + + fn from_str(s: &str) -> Result { + Ok(Self(s.parse::()?)) + } +} + +impl Display for RoleIndex { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { + write!(f, "{}", self.0) + } +} diff --git a/rust/signed_doc/src/signature/mod.rs b/rust/signed_doc/src/signature/mod.rs index 77fc98fcb9b..d48e636d2b9 100644 --- a/rust/signed_doc/src/signature/mod.rs +++ b/rust/signed_doc/src/signature/mod.rs @@ -1,4 +1,4 @@ //! Catalyst Signed Document COSE Signature information. mod kid; -pub use kid::Kid; +pub use kid::KidURI; From 276b4ac0beebd2f3058ee22c920b4ed70e611301 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Rosales?= Date: Sun, 5 Jan 2025 16:29:54 -0600 Subject: [PATCH 27/71] feat(rust/signed-doc): add mod payload --- rust/signed_doc/src/lib.rs | 6 ++---- rust/signed_doc/src/payload/mod.rs | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 4 deletions(-) create mode 100644 rust/signed_doc/src/payload/mod.rs diff --git a/rust/signed_doc/src/lib.rs b/rust/signed_doc/src/lib.rs index 01f13543fa9..15df2a8a415 100644 --- a/rust/signed_doc/src/lib.rs +++ b/rust/signed_doc/src/lib.rs @@ -9,9 +9,11 @@ use std::{ use coset::{CborSerializable, TaggedCborSerializable}; mod metadata; +mod payload; mod signature; pub use metadata::{DocumentRef, Metadata, UuidV7}; +use payload::Content; pub use signature::KidURI; /// Keep all the contents private. @@ -60,10 +62,6 @@ struct InnerCatalystSignedDocument { content_errors: Vec, } -// Do this instead of `new` if we are converting a single parameter into a struct/type we -// should use either `From` or `TryFrom` and reserve `new` for cases where we need -// multiple parameters to actually create the type. This is much more elegant to use this -// way, in code. impl TryFrom> for CatalystSignedDocument { type Error = anyhow::Error; diff --git a/rust/signed_doc/src/payload/mod.rs b/rust/signed_doc/src/payload/mod.rs new file mode 100644 index 00000000000..0b99b1f338f --- /dev/null +++ b/rust/signed_doc/src/payload/mod.rs @@ -0,0 +1,19 @@ +//! Catalyst Signed Document JSON Payload + +use std::fmt::{Display, Formatter}; + +/// JSON Content +#[derive(Debug, Default)] +pub struct Content(serde_json::Value); + +impl From for Content { + fn from(value: serde_json::Value) -> Self { + Self(value) + } +} + +impl Display for Content { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { + write!(f, "{}", self.0) + } +} From 9c94aa59bc7b2fbae7cfff9c07dca1b03c40fb86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Rosales?= Date: Sun, 5 Jan 2025 16:58:17 -0600 Subject: [PATCH 28/71] chore(rust/signed-doc): tidy up metadata mod --- rust/signed_doc/src/metadata/mod.rs | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/rust/signed_doc/src/metadata/mod.rs b/rust/signed_doc/src/metadata/mod.rs index 95789641767..3a3d9949bc4 100644 --- a/rust/signed_doc/src/metadata/mod.rs +++ b/rust/signed_doc/src/metadata/mod.rs @@ -21,6 +21,8 @@ pub use uuid_type::{UuidV4, UuidV7}; const CONTENT_ENCODING_KEY: &str = "Content-Encoding"; /// Document Metadata. +/// +/// These values are extracted from the COSE Sign protected header. #[derive(Debug, serde::Deserialize)] pub struct Metadata { /// Document Type `UUIDv4`. @@ -102,18 +104,24 @@ impl Metadata { pub fn content_errors(&self) -> &Vec { &self.content_errors } + + /// Return + #[must_use] + pub fn content_type(&self) -> &Option { + &self.content_type + } } impl Display for Metadata { fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { writeln!(f, "Metadata {{")?; - writeln!(f, " doc_type: {},", self.doc_type)?; - writeln!(f, " doc_id: {},", self.id)?; - writeln!(f, " doc_ver: {},", self.ver)?; - writeln!(f, " doc_ref: {:?},", self.doc_ref)?; - writeln!(f, " doc_template: {:?},", self.template)?; - writeln!(f, " doc_reply: {:?},", self.reply)?; - writeln!(f, " doc_section: {:?}", self.section)?; + writeln!(f, " type: {},", self.doc_type)?; + writeln!(f, " id: {},", self.id)?; + writeln!(f, " ver: {},", self.ver)?; + writeln!(f, " ref: {:?},", self.doc_ref)?; + writeln!(f, " template: {:?},", self.template)?; + writeln!(f, " reply: {:?},", self.reply)?; + writeln!(f, " section: {:?}", self.section)?; writeln!(f, " content_type: {:?}", self.content_type)?; writeln!(f, " content_encoding: {:?}", self.content_encoding)?; writeln!(f, "}}") From e7f651f6c18c558bb802869dadc366cb882d9c93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Rosales?= Date: Sun, 5 Jan 2025 17:00:47 -0600 Subject: [PATCH 29/71] wip(rust/signed_doc): use Content type for document payload --- rust/signed_doc/src/lib.rs | 43 +++++++++++++++++++++++--------------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/rust/signed_doc/src/lib.rs b/rust/signed_doc/src/lib.rs index 15df2a8a415..62d4b1a950d 100644 --- a/rust/signed_doc/src/lib.rs +++ b/rust/signed_doc/src/lib.rs @@ -16,6 +16,23 @@ pub use metadata::{DocumentRef, Metadata, UuidV7}; use payload::Content; pub use signature::KidURI; +/// Inner type that holds the Catalyst Signed Document with parsing errors. +#[derive(Default)] +struct InnerCatalystSignedDocument { + /// Document Metadata + metadata: Metadata, + /// Document Payload JSON Content + payload: Content, + /// Signatures + signatures: Vec, + /// Raw COSE Sign data + cose_sign: coset::CoseSign, + /// Raw COSE Sign bytes + cose_bytes: Vec, + /// Content Errors found when parsing the Document + content_errors: Vec, +} + /// Keep all the contents private. /// Better even to use a structure like this. Wrapping in an Arc means we don't have to /// manage the Arc anywhere else. These are likely to be large, best to have the Arc be @@ -28,7 +45,7 @@ pub struct CatalystSignedDocument { impl Display for CatalystSignedDocument { fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { writeln!(f, "{}", self.inner.metadata)?; - writeln!(f, "JSON Payload {:#}\n", self.inner.payload)?; + writeln!(f, "{:#?}\n", self.inner.payload)?; writeln!(f, "Signature Information [")?; for signature in &self.inner.signatures { writeln!( @@ -47,21 +64,6 @@ impl Display for CatalystSignedDocument { } } -#[derive(Default)] -/// Inner type that holds the Catalyst Signed Document with parsing errors. -struct InnerCatalystSignedDocument { - /// Document Metadata - metadata: Metadata, - /// JSON Payload - payload: serde_json::Value, - /// Signatures - signatures: Vec, - /// Raw COSE Sign data - cose_sign: coset::CoseSign, - /// Content Errors found when parsing the Document - content_errors: Vec, -} - impl TryFrom> for CatalystSignedDocument { type Error = anyhow::Error; @@ -91,9 +93,10 @@ impl TryFrom> for CatalystSignedDocument { let signatures = cose.signatures.clone(); let inner = InnerCatalystSignedDocument { metadata, - payload, + payload: payload.into(), signatures, cose_sign: cose, + cose_bytes, content_errors, }; Ok(CatalystSignedDocument { @@ -152,4 +155,10 @@ impl CatalystSignedDocument { pub fn doc_section(&self) -> Option { self.inner.metadata.doc_section() } + + /// Return Raw COSE SIGN bytes. + #[must_use] + pub fn cose_sign_bytes(&self) -> &[u8] { + self.inner.cose_bytes.as_ref() + } } From aa69d7bd09855e0c49e73a5faede4eff3bee78c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Rosales?= Date: Sun, 5 Jan 2025 23:37:18 -0600 Subject: [PATCH 30/71] fix(rust/signed_doc): ser/de for ContentType and ContentEncoding. * update example to build docs from metadata file --- rust/signed_doc/examples/mk_signed_doc.rs | 19 +++++- rust/signed_doc/meta.schema.json | 13 ++++ .../src/metadata/content_encoding.rs | 48 +++++++++++---- rust/signed_doc/src/metadata/content_type.rs | 59 ++++++++++++++++--- rust/signed_doc/src/metadata/mod.rs | 23 +++++--- 5 files changed, 133 insertions(+), 29 deletions(-) diff --git a/rust/signed_doc/examples/mk_signed_doc.rs b/rust/signed_doc/examples/mk_signed_doc.rs index 21f34f88bd2..1f0ecc9a9d5 100644 --- a/rust/signed_doc/examples/mk_signed_doc.rs +++ b/rust/signed_doc/examples/mk_signed_doc.rs @@ -9,7 +9,7 @@ use std::{ }; use clap::Parser; -use coset::CborSerializable; +use coset::{iana::CoapContentFormat, CborSerializable}; use ed25519_dalek::{ ed25519::signature::Signer, pkcs8::{DecodePrivateKey, DecodePublicKey}, @@ -121,6 +121,7 @@ impl Cli { let json_doc = load_json_from_file(&doc)?; let json_meta = load_json_from_file(&meta) .map_err(|e| anyhow::anyhow!("Failed to load metadata from file: {e}"))?; + println!("{json_meta}"); validate_json(&json_doc, &doc_schema)?; let compressed_doc = brotli_compress_json(&json_doc)?; let empty_cose_sign = build_empty_cose_doc(compressed_doc, &json_meta); @@ -194,7 +195,7 @@ fn brotli_decompress_json(mut doc_bytes: &[u8]) -> anyhow::Result coset::Header { coset::HeaderBuilder::new() - .content_format(coset::iana::CoapContentFormat::Json) + .content_format(CoapContentFormat::Json) .text_value( CONTENT_ENCODING_KEY.to_string(), CONTENT_ENCODING_VALUE.to_string().into(), @@ -203,7 +204,19 @@ fn cose_protected_header() -> coset::Header { } fn build_empty_cose_doc(doc_bytes: Vec, meta: &Metadata) -> coset::CoseSign { - let mut protected_header = cose_protected_header(); + let mut builder = coset::HeaderBuilder::new(); + + if let Some(content_type) = meta.content_type() { + builder = builder.content_format(CoapContentFormat::from(content_type)); + } + + if let Some(content_encoding) = meta.content_encoding() { + builder = builder.text_value( + CONTENT_ENCODING_KEY.to_string(), + format!("{content_encoding}").into(), + ); + } + let mut protected_header = builder.build(); protected_header.rest.push(( coset::Label::Text("type".to_string()), diff --git a/rust/signed_doc/meta.schema.json b/rust/signed_doc/meta.schema.json index c1547308964..0a7cd8f85e7 100644 --- a/rust/signed_doc/meta.schema.json +++ b/rust/signed_doc/meta.schema.json @@ -97,6 +97,19 @@ }, "section": { "type": "string" + }, + "content-type": { + "type": "string", + "examples": [ + "json", + "cbor" + ] + }, + "content-encoding": { + "type": "string", + "examples": [ + "br" + ] } }, "required": [ diff --git a/rust/signed_doc/src/metadata/content_encoding.rs b/rust/signed_doc/src/metadata/content_encoding.rs index 3309df87a37..7126b9853a2 100644 --- a/rust/signed_doc/src/metadata/content_encoding.rs +++ b/rust/signed_doc/src/metadata/content_encoding.rs @@ -1,30 +1,56 @@ //! Document Payload Content Encoding. +use std::{ + fmt::{Display, Formatter}, + str::FromStr, +}; + +use serde::{de, Deserialize, Deserializer}; + /// Catalyst Signed Document Content Encoding Key. const CONTENT_ENCODING_KEY: &str = "Content-Encoding"; /// IANA `CoAP` Content Encoding. -#[derive(Debug, serde::Deserialize)] -#[serde(untagged)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum ContentEncoding { /// Brotli compression.format. - #[serde(rename = "br")] Brotli, } +impl Display for ContentEncoding { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { + match self { + Self::Brotli => write!(f, "br"), + } + } +} + +impl FromStr for ContentEncoding { + type Err = anyhow::Error; + + fn from_str(encoding: &str) -> Result { + match encoding { + "br" => Ok(ContentEncoding::Brotli), + _ => anyhow::bail!("Unsupported Content Encoding: {encoding:?}"), + } + } +} + +impl<'de> Deserialize<'de> for ContentEncoding { + fn deserialize(deserializer: D) -> Result + where D: Deserializer<'de> { + let s = String::deserialize(deserializer)?; + FromStr::from_str(&s).map_err(de::Error::custom) + } +} + impl TryFrom<&coset::cbor::Value> for ContentEncoding { type Error = anyhow::Error; - #[allow(clippy::todo)] fn try_from(val: &coset::cbor::Value) -> anyhow::Result { match val.as_text() { - Some(encoding) => { - match encoding.to_string().to_lowercase().as_ref() { - "br" => Ok(ContentEncoding::Brotli), - _ => anyhow::bail!("Unsupported Content Encoding: {encoding}"), - } - }, - _ => { + Some(encoding) => encoding.parse(), + None => { anyhow::bail!("Expected Content Encoding to be a string"); }, } diff --git a/rust/signed_doc/src/metadata/content_type.rs b/rust/signed_doc/src/metadata/content_type.rs index 782027741d3..06ab73b3eaf 100644 --- a/rust/signed_doc/src/metadata/content_type.rs +++ b/rust/signed_doc/src/metadata/content_type.rs @@ -1,8 +1,15 @@ //! Document Payload Content Type. +use std::{ + fmt::{Display, Formatter}, + str::FromStr, +}; + +use coset::iana::CoapContentFormat; +use serde::{de, Deserialize, Deserializer}; + /// Payload Content Type. -#[derive(Debug, serde::Deserialize)] -#[serde(untagged, rename_all_fields = "lowercase")] +#[derive(Copy, Clone, Debug)] pub enum ContentType { /// 'application/cbor' Cbor, @@ -10,15 +17,53 @@ pub enum ContentType { Json, } +impl Display for ContentType { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { + match self { + Self::Cbor => write!(f, "cbor"), + Self::Json => write!(f, "json"), + } + } +} + +impl FromStr for ContentType { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + match s { + "cbor" => Ok(Self::Cbor), + "json" => Ok(Self::Json), + _ => anyhow::bail!("Unsupported Content Type: {s:?}"), + } + } +} + +impl<'de> Deserialize<'de> for ContentType { + fn deserialize(deserializer: D) -> Result + where D: Deserializer<'de> { + let s = String::deserialize(deserializer)?; + FromStr::from_str(&s).map_err(de::Error::custom) + } +} + +impl From for CoapContentFormat { + fn from(value: ContentType) -> Self { + match value { + ContentType::Cbor => Self::Cbor, + ContentType::Json => Self::Json, + } + } +} + impl TryFrom<&coset::ContentType> for ContentType { type Error = anyhow::Error; fn try_from(value: &coset::ContentType) -> Result { - use coset::iana::CoapContentFormat as Format; - match value { - coset::ContentType::Assigned(Format::Json) => Ok(ContentType::Json), - coset::ContentType::Assigned(Format::Cbor) => Ok(ContentType::Cbor), + let content_type = match value { + coset::ContentType::Assigned(CoapContentFormat::Json) => ContentType::Json, + coset::ContentType::Assigned(CoapContentFormat::Cbor) => ContentType::Cbor, _ => anyhow::bail!("Unsupported Content Type {value:?}"), - } + }; + Ok(content_type) } } diff --git a/rust/signed_doc/src/metadata/mod.rs b/rust/signed_doc/src/metadata/mod.rs index 3a3d9949bc4..cbb4675112c 100644 --- a/rust/signed_doc/src/metadata/mod.rs +++ b/rust/signed_doc/src/metadata/mod.rs @@ -42,8 +42,10 @@ pub struct Metadata { /// Reference to the document section. section: Option, /// Document Payload Content Type. + #[serde(default, rename = "content-type")] content_type: Option, /// Document Payload Content Encoding. + #[serde(default, rename = "content-encoding")] content_encoding: Option, /// Metadata Content Errors #[serde(skip)] @@ -105,10 +107,16 @@ impl Metadata { &self.content_errors } - /// Return + /// Returns the Document Content Type, if any. #[must_use] - pub fn content_type(&self) -> &Option { - &self.content_type + pub fn content_type(&self) -> Option { + self.content_type + } + + /// Returns the Document Content Encoding, if any. + #[must_use] + pub fn content_encoding(&self) -> Option { + self.content_encoding } } @@ -194,11 +202,10 @@ impl From<&coset::ProtectedHeader> for Metadata { }, } }, - _ => { - errors.push( - "Invalid COSE document protected header '{CONTENT_ENCODING_KEY}' label" - .to_string(), - ); + None => { + errors.push(format!( + "Invalid COSE document protected header '{CONTENT_ENCODING_KEY}' is missing" + )); }, } match cose_protected_header_find(protected, "type") { From 97af0ff4477d00c99743c440d50731ae5e8df006 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Rosales?= Date: Mon, 6 Jan 2025 21:48:13 -0600 Subject: [PATCH 31/71] fix(rust/signed_doc): update metadata to require content type * ContentType::Json is default --- rust/signed_doc/examples/mk_signed_doc.rs | 5 +---- rust/signed_doc/meta.schema.json | 3 ++- rust/signed_doc/src/metadata/content_type.rs | 6 ++++++ rust/signed_doc/src/metadata/mod.rs | 20 ++++++++++---------- 4 files changed, 19 insertions(+), 15 deletions(-) diff --git a/rust/signed_doc/examples/mk_signed_doc.rs b/rust/signed_doc/examples/mk_signed_doc.rs index 1f0ecc9a9d5..c936b13d0b3 100644 --- a/rust/signed_doc/examples/mk_signed_doc.rs +++ b/rust/signed_doc/examples/mk_signed_doc.rs @@ -204,10 +204,7 @@ fn cose_protected_header() -> coset::Header { } fn build_empty_cose_doc(doc_bytes: Vec, meta: &Metadata) -> coset::CoseSign { - let mut builder = coset::HeaderBuilder::new(); - - if let Some(content_type) = meta.content_type() { - builder = builder.content_format(CoapContentFormat::from(content_type)); + let mut builder = coset::HeaderBuilder::new().content_format(CoapContentFormat::from(meta.content_type())); } if let Some(content_encoding) = meta.content_encoding() { diff --git a/rust/signed_doc/meta.schema.json b/rust/signed_doc/meta.schema.json index 0a7cd8f85e7..c3fa82d4f00 100644 --- a/rust/signed_doc/meta.schema.json +++ b/rust/signed_doc/meta.schema.json @@ -115,6 +115,7 @@ "required": [ "type", "id", - "ver" + "ver", + "content-type" ] } diff --git a/rust/signed_doc/src/metadata/content_type.rs b/rust/signed_doc/src/metadata/content_type.rs index 06ab73b3eaf..c2ce21f0604 100644 --- a/rust/signed_doc/src/metadata/content_type.rs +++ b/rust/signed_doc/src/metadata/content_type.rs @@ -17,6 +17,12 @@ pub enum ContentType { Json, } +impl Default for ContentType { + fn default() -> Self { + Self::Json + } +} + impl Display for ContentType { fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { match self { diff --git a/rust/signed_doc/src/metadata/mod.rs b/rust/signed_doc/src/metadata/mod.rs index cbb4675112c..78ebd96c08b 100644 --- a/rust/signed_doc/src/metadata/mod.rs +++ b/rust/signed_doc/src/metadata/mod.rs @@ -32,6 +32,12 @@ pub struct Metadata { id: DocumentId, /// Document Version `UUIDv7`. ver: DocumentVersion, + /// Document Payload Content Type. + #[serde(default, rename = "content-type")] + content_type: ContentType, + /// Document Payload Content Encoding. + #[serde(default, rename = "content-encoding")] + content_encoding: Option, /// Reference to the latest document. #[serde(rename = "ref")] doc_ref: Option, @@ -41,12 +47,6 @@ pub struct Metadata { reply: Option, /// Reference to the document section. section: Option, - /// Document Payload Content Type. - #[serde(default, rename = "content-type")] - content_type: Option, - /// Document Payload Content Encoding. - #[serde(default, rename = "content-encoding")] - content_encoding: Option, /// Metadata Content Errors #[serde(skip)] content_errors: Vec, @@ -109,7 +109,7 @@ impl Metadata { /// Returns the Document Content Type, if any. #[must_use] - pub fn content_type(&self) -> Option { + pub fn content_type(&self) -> ContentType { self.content_type } @@ -130,7 +130,7 @@ impl Display for Metadata { writeln!(f, " template: {:?},", self.template)?; writeln!(f, " reply: {:?},", self.reply)?; writeln!(f, " section: {:?}", self.section)?; - writeln!(f, " content_type: {:?}", self.content_type)?; + writeln!(f, " content_type: {}", self.content_type)?; writeln!(f, " content_encoding: {:?}", self.content_encoding)?; writeln!(f, "}}") } @@ -146,7 +146,7 @@ impl Default for Metadata { template: None, reply: None, section: None, - content_type: None, + content_type: ContentType::default(), content_encoding: None, content_errors: Vec::new(), } @@ -173,7 +173,7 @@ impl From<&coset::ProtectedHeader> for Metadata { match protected.header.content_type.as_ref() { Some(iana_content_type) => { match ContentType::try_from(iana_content_type) { - Ok(content_type) => metadata.content_type = Some(content_type), + Ok(content_type) => metadata.content_type = content_type, Err(e) => { errors.push(format!("Invalid Document Content-Type: {e}")); }, From 1486b87bd8bbee8727cef7ca868bb8dac58fa5f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Rosales?= Date: Wed, 8 Jan 2025 00:06:54 -0600 Subject: [PATCH 32/71] fix(rust/signed_doc): update payload type --- rust/signed_doc/src/lib.rs | 27 ++++++++++--------- rust/signed_doc/src/payload/json.rs | 41 +++++++++++++++++++++++++++++ rust/signed_doc/src/payload/mod.rs | 23 ++++++++++++---- 3 files changed, 74 insertions(+), 17 deletions(-) create mode 100644 rust/signed_doc/src/payload/json.rs diff --git a/rust/signed_doc/src/lib.rs b/rust/signed_doc/src/lib.rs index 62d4b1a950d..e31d3212016 100644 --- a/rust/signed_doc/src/lib.rs +++ b/rust/signed_doc/src/lib.rs @@ -1,5 +1,4 @@ //! Catalyst documents signing crate -#![allow(dead_code)] use std::{ convert::TryFrom, fmt::{Display, Formatter}, @@ -13,7 +12,7 @@ mod payload; mod signature; pub use metadata::{DocumentRef, Metadata, UuidV7}; -use payload::Content; +use payload::JsonContent; pub use signature::KidURI; /// Inner type that holds the Catalyst Signed Document with parsing errors. @@ -21,8 +20,8 @@ pub use signature::KidURI; struct InnerCatalystSignedDocument { /// Document Metadata metadata: Metadata, - /// Document Payload JSON Content - payload: Content, + /// Document Payload viewed as JSON Content + payload: JsonContent, /// Signatures signatures: Vec, /// Raw COSE Sign data @@ -81,19 +80,23 @@ impl TryFrom> for CatalystSignedDocument { content_errors.extend_from_slice(metadata.content_errors()); } - let payload = if let Some(payload) = &cose.payload { - let mut buf = Vec::new(); - let mut bytes = payload.as_slice(); - brotli::BrotliDecompress(&mut bytes, &mut buf)?; - serde_json::from_slice(&buf)? + let mut payload = JsonContent::default(); + + if let Some(bytes) = &cose.payload { + match JsonContent::try_from((bytes, metadata.content_encoding())) { + Ok(c) => payload = c, + Err(e) => { + content_errors.push(format!("Invalid Payload: {e}")); + }, + } } else { - println!("COSE missing payload field with the JSON content in it"); - serde_json::Value::Object(serde_json::Map::new()) + content_errors.push("COSE payload is empty".to_string()); }; + let signatures = cose.signatures.clone(); let inner = InnerCatalystSignedDocument { metadata, - payload: payload.into(), + payload, signatures, cose_sign: cose, cose_bytes, diff --git a/rust/signed_doc/src/payload/json.rs b/rust/signed_doc/src/payload/json.rs new file mode 100644 index 00000000000..51be6193516 --- /dev/null +++ b/rust/signed_doc/src/payload/json.rs @@ -0,0 +1,41 @@ +//! JSON Content +use super::Content; +use crate::metadata::ContentEncoding; + +/// JSON encoded content +pub type Json = Content; + +impl Default for Json { + fn default() -> Self { + serde_json::Value::Object(serde_json::Map::new()).into() + } +} + +impl TryFrom<&[u8]> for Content { + type Error = anyhow::Error; + + fn try_from(value: &[u8]) -> Result { + serde_json::from_slice(value) + .map_err(|e| anyhow::anyhow!("Failed to parse any JSON content: {e}")) + } +} + +impl TryFrom<(&Vec, Option)> for Content { + type Error = anyhow::Error; + + fn try_from( + (value, encoding): (&Vec, Option), + ) -> Result { + if let Some(content_encoding) = encoding { + match content_encoding.decode(value) { + Ok(decompressed) => { + return Self::try_from(decompressed.as_slice()); + }, + Err(e) => { + anyhow::bail!("Failed to decode {encoding:?}: {e}"); + }, + } + } + Self::try_from(value.as_ref()) + } +} diff --git a/rust/signed_doc/src/payload/mod.rs b/rust/signed_doc/src/payload/mod.rs index 0b99b1f338f..06d43aac31b 100644 --- a/rust/signed_doc/src/payload/mod.rs +++ b/rust/signed_doc/src/payload/mod.rs @@ -2,18 +2,31 @@ use std::fmt::{Display, Formatter}; +use serde::{Deserialize, Deserializer}; + +mod json; + +pub use json::Json as JsonContent; + /// JSON Content -#[derive(Debug, Default)] -pub struct Content(serde_json::Value); +#[derive(Debug)] +pub struct Content(T); -impl From for Content { - fn from(value: serde_json::Value) -> Self { +impl From for Content { + fn from(value: T) -> Self { Self(value) } } -impl Display for Content { +impl Display for Content { fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { write!(f, "{}", self.0) } } + +impl<'de, T: serde::Deserialize<'de>> Deserialize<'de> for Content { + fn deserialize(deserializer: D) -> Result + where D: Deserializer<'de> { + T::deserialize(deserializer).map(std::convert::Into::into) + } +} From f92061978d87b12934e9b7d356cf878afaedf11a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Rosales?= Date: Wed, 8 Jan 2025 00:09:17 -0600 Subject: [PATCH 33/71] fix(rust/signed_doc): add brotli decompression --- .../signed_doc/src/metadata/content_encoding.rs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/rust/signed_doc/src/metadata/content_encoding.rs b/rust/signed_doc/src/metadata/content_encoding.rs index 7126b9853a2..1f4fd0444c2 100644 --- a/rust/signed_doc/src/metadata/content_encoding.rs +++ b/rust/signed_doc/src/metadata/content_encoding.rs @@ -7,9 +7,6 @@ use std::{ use serde::{de, Deserialize, Deserializer}; -/// Catalyst Signed Document Content Encoding Key. -const CONTENT_ENCODING_KEY: &str = "Content-Encoding"; - /// IANA `CoAP` Content Encoding. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum ContentEncoding { @@ -56,3 +53,17 @@ impl TryFrom<&coset::cbor::Value> for ContentEncoding { } } } + +impl ContentEncoding { + /// Decompress a Brotli payload + pub fn decode(self, payload: &Vec) -> anyhow::Result> { + match self { + Self::Brotli => { + let mut buf = Vec::new(); + let mut bytes = payload.as_slice(); + brotli::BrotliDecompress(&mut bytes, &mut buf)?; + Ok(buf) + }, + } + } +} From 55dd3f5b0add055c1af122d4282dd6eb4fed8712 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Rosales?= Date: Wed, 8 Jan 2025 00:10:39 -0600 Subject: [PATCH 34/71] fix(rust/signed_doc): add metadata fields --- rust/signed_doc/examples/mk_signed_doc.rs | 4 +- rust/signed_doc/src/metadata/document_id.rs | 5 - rust/signed_doc/src/metadata/document_type.rs | 5 - .../src/metadata/document_version.rs | 5 - rust/signed_doc/src/metadata/mod.rs | 97 ++++++++++--------- .../src/metadata/uuid_type/uuid_v4.rs | 5 - 6 files changed, 51 insertions(+), 70 deletions(-) diff --git a/rust/signed_doc/examples/mk_signed_doc.rs b/rust/signed_doc/examples/mk_signed_doc.rs index c936b13d0b3..84b4adf27a3 100644 --- a/rust/signed_doc/examples/mk_signed_doc.rs +++ b/rust/signed_doc/examples/mk_signed_doc.rs @@ -204,8 +204,8 @@ fn cose_protected_header() -> coset::Header { } fn build_empty_cose_doc(doc_bytes: Vec, meta: &Metadata) -> coset::CoseSign { - let mut builder = coset::HeaderBuilder::new().content_format(CoapContentFormat::from(meta.content_type())); - } + let mut builder = + coset::HeaderBuilder::new().content_format(CoapContentFormat::from(meta.content_type())); if let Some(content_encoding) = meta.content_encoding() { builder = builder.text_value( diff --git a/rust/signed_doc/src/metadata/document_id.rs b/rust/signed_doc/src/metadata/document_id.rs index 84180908222..f21d334f1d3 100644 --- a/rust/signed_doc/src/metadata/document_id.rs +++ b/rust/signed_doc/src/metadata/document_id.rs @@ -19,11 +19,6 @@ impl DocumentId { } } - /// Check if this is a valid `UUIDv7`. - pub fn is_valid(&self) -> bool { - self.uuid.is_valid() - } - /// Returns the `uuid::Uuid` type. #[must_use] pub fn uuid(&self) -> uuid::Uuid { diff --git a/rust/signed_doc/src/metadata/document_type.rs b/rust/signed_doc/src/metadata/document_type.rs index 3aa0fddfe6c..27e8e3e0459 100644 --- a/rust/signed_doc/src/metadata/document_type.rs +++ b/rust/signed_doc/src/metadata/document_type.rs @@ -14,11 +14,6 @@ impl DocumentType { Self(UuidV4::invalid()) } - /// Check if this is a valid `UUIDv4`. - pub fn is_valid(&self) -> bool { - self.0.is_valid() - } - /// Returns the `uuid::Uuid` type. #[must_use] pub fn uuid(&self) -> uuid::Uuid { diff --git a/rust/signed_doc/src/metadata/document_version.rs b/rust/signed_doc/src/metadata/document_version.rs index 21e44ccd8b3..ed483d145c4 100644 --- a/rust/signed_doc/src/metadata/document_version.rs +++ b/rust/signed_doc/src/metadata/document_version.rs @@ -13,11 +13,6 @@ impl DocumentVersion { Self(UuidV7::invalid()) } - /// Check if this is a valid `UUIDv7`. - pub fn is_valid(&self) -> bool { - self.0.is_valid() - } - /// Returns the `uuid::Uuid` type. #[must_use] pub fn uuid(&self) -> uuid::Uuid { diff --git a/rust/signed_doc/src/metadata/mod.rs b/rust/signed_doc/src/metadata/mod.rs index 78ebd96c08b..3c4fda59c78 100644 --- a/rust/signed_doc/src/metadata/mod.rs +++ b/rust/signed_doc/src/metadata/mod.rs @@ -38,21 +38,39 @@ pub struct Metadata { /// Document Payload Content Encoding. #[serde(default, rename = "content-encoding")] content_encoding: Option, + /// Additional Metadata Fields. + #[serde(flatten)] + extra: Fields, + /// Metadata Content Errors + #[serde(skip)] + content_errors: Vec, +} + +/// Optional Metadata Fields. +/// +/// These values are extracted from the COSE Sign protected header labels. +#[derive(Default, Debug, serde::Deserialize)] +struct Fields { /// Reference to the latest document. #[serde(rename = "ref")] doc_ref: Option, + /// Hash of the referenced document bytes. + ref_hash: Option>, /// Reference to the document template. template: Option, /// Reference to the document reply. reply: Option, /// Reference to the document section. section: Option, - /// Metadata Content Errors - #[serde(skip)] - content_errors: Vec, } impl Metadata { + /// Are there any validation errors (as opposed to structural errors). + #[must_use] + pub fn has_error(&self) -> bool { + !self.content_errors.is_empty() + } + /// Return Document Type `UUIDv4`. #[must_use] pub fn doc_type(&self) -> uuid::Uuid { @@ -71,34 +89,46 @@ impl Metadata { self.ver.uuid() } + /// Returns the Document Content Type, if any. + #[must_use] + pub fn content_type(&self) -> ContentType { + self.content_type + } + + /// Returns the Document Content Encoding, if any. + #[must_use] + pub fn content_encoding(&self) -> Option { + self.content_encoding + } + + /// Return Last Document Reference `Option>`. + #[must_use] + pub fn doc_ref_hash(&self) -> Option> { + self.extra.ref_hash.clone() + } + /// Return Last Document Reference `Option`. #[must_use] pub fn doc_ref(&self) -> Option { - self.doc_ref + self.extra.doc_ref } /// Return Document Template `Option`. #[must_use] pub fn doc_template(&self) -> Option { - self.template + self.extra.template } /// Return Document Reply `Option`. #[must_use] pub fn doc_reply(&self) -> Option { - self.reply + self.extra.reply } /// Return Document Section `Option`. #[must_use] pub fn doc_section(&self) -> Option { - self.section.clone() - } - - /// Are there any validation errors (as opposed to structural errors). - #[must_use] - pub fn has_error(&self) -> bool { - !self.content_errors.is_empty() + self.extra.section.clone() } /// List of Content Errors. @@ -106,18 +136,6 @@ impl Metadata { pub fn content_errors(&self) -> &Vec { &self.content_errors } - - /// Returns the Document Content Type, if any. - #[must_use] - pub fn content_type(&self) -> ContentType { - self.content_type - } - - /// Returns the Document Content Encoding, if any. - #[must_use] - pub fn content_encoding(&self) -> Option { - self.content_encoding - } } impl Display for Metadata { @@ -126,12 +144,9 @@ impl Display for Metadata { writeln!(f, " type: {},", self.doc_type)?; writeln!(f, " id: {},", self.id)?; writeln!(f, " ver: {},", self.ver)?; - writeln!(f, " ref: {:?},", self.doc_ref)?; - writeln!(f, " template: {:?},", self.template)?; - writeln!(f, " reply: {:?},", self.reply)?; - writeln!(f, " section: {:?}", self.section)?; writeln!(f, " content_type: {}", self.content_type)?; writeln!(f, " content_encoding: {:?}", self.content_encoding)?; + writeln!(f, " additional_fields: {:?},", self.extra)?; writeln!(f, "}}") } } @@ -142,28 +157,14 @@ impl Default for Metadata { doc_type: DocumentType::invalid(), id: DocumentId::invalid(), ver: DocumentVersion::invalid(), - doc_ref: None, - template: None, - reply: None, - section: None, content_type: ContentType::default(), content_encoding: None, + extra: Fields::default(), content_errors: Vec::new(), } } } -/// Errors found when decoding content. -#[derive(Default, Debug)] -struct ContentErrors(Vec); - -impl ContentErrors { - /// Appends an element to the back of the collection - fn push(&mut self, error_string: String) { - self.0.push(error_string); - } -} - impl From<&coset::ProtectedHeader> for Metadata { #[allow(clippy::too_many_lines)] fn from(protected: &coset::ProtectedHeader) -> Self { @@ -261,7 +262,7 @@ impl From<&coset::ProtectedHeader> for Metadata { if let Some(cbor_doc_ref) = cose_protected_header_find(protected, "ref") { match DocumentRef::try_from(&cbor_doc_ref) { Ok(doc_ref) => { - metadata.doc_ref = Some(doc_ref); + metadata.extra.doc_ref = Some(doc_ref); }, Err(e) => { errors.push(format!( @@ -274,7 +275,7 @@ impl From<&coset::ProtectedHeader> for Metadata { if let Some(cbor_doc_template) = cose_protected_header_find(protected, "template") { match DocumentRef::try_from(&cbor_doc_template) { Ok(doc_template) => { - metadata.template = Some(doc_template); + metadata.extra.template = Some(doc_template); }, Err(e) => { errors.push(format!( @@ -287,7 +288,7 @@ impl From<&coset::ProtectedHeader> for Metadata { if let Some(cbor_doc_reply) = cose_protected_header_find(protected, "reply") { match DocumentRef::try_from(&cbor_doc_reply) { Ok(doc_reply) => { - metadata.reply = Some(doc_reply); + metadata.extra.reply = Some(doc_reply); }, Err(e) => { errors.push(format!( @@ -300,7 +301,7 @@ impl From<&coset::ProtectedHeader> for Metadata { if let Some(cbor_doc_section) = cose_protected_header_find(protected, "section") { match cbor_doc_section.into_text() { Ok(doc_section) => { - metadata.section = Some(doc_section); + metadata.extra.section = Some(doc_section); }, Err(e) => { errors.push(format!( diff --git a/rust/signed_doc/src/metadata/uuid_type/uuid_v4.rs b/rust/signed_doc/src/metadata/uuid_type/uuid_v4.rs index fba37b699d2..ef409e94bd8 100644 --- a/rust/signed_doc/src/metadata/uuid_type/uuid_v4.rs +++ b/rust/signed_doc/src/metadata/uuid_type/uuid_v4.rs @@ -20,11 +20,6 @@ impl UuidV4 { Self { uuid: INVALID_UUID } } - /// Check if this is a valid `UUIDv4`. - pub fn is_valid(&self) -> bool { - self.uuid != INVALID_UUID && self.uuid.get_version_num() == Self::UUID_VERSION_NUMBER - } - /// Returns the `uuid::Uuid` type. #[must_use] pub fn uuid(&self) -> uuid::Uuid { From d38f82bc84809664918a4184d5bc3b8c4bdba359 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Rosales?= Date: Wed, 8 Jan 2025 00:24:11 -0600 Subject: [PATCH 35/71] wip(rust/signed_doc): use catalyst-types crate for kid uri --- rust/signed_doc/Cargo.toml | 3 ++- rust/signed_doc/examples/mk_signed_doc.rs | 6 +++--- rust/signed_doc/src/lib.rs | 2 +- rust/signed_doc/src/signature/mod.rs | 4 +--- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/rust/signed_doc/Cargo.toml b/rust/signed_doc/Cargo.toml index cf5dc6e17bf..0abe5e8e5ec 100644 --- a/rust/signed_doc/Cargo.toml +++ b/rust/signed_doc/Cargo.toml @@ -12,6 +12,7 @@ workspace = true [dependencies] cardano-blockchain-types = { version = "0.0.1", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "v0.0.11" } +catalyst-types = { version = "0.0.1", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250107-00" } anyhow = "1.0.95" serde = { version = "1.0.217", features = ["derive"] } serde_json = "1.0.134" @@ -28,4 +29,4 @@ base64-url = "3.0.0" [dev-dependencies] clap = { version = "4.5.23", features = ["derive", "env"] } -rand = "0.8.5" \ No newline at end of file +rand = "0.8.5" diff --git a/rust/signed_doc/examples/mk_signed_doc.rs b/rust/signed_doc/examples/mk_signed_doc.rs index 84b4adf27a3..91cd64b8625 100644 --- a/rust/signed_doc/examples/mk_signed_doc.rs +++ b/rust/signed_doc/examples/mk_signed_doc.rs @@ -9,12 +9,12 @@ use std::{ }; use clap::Parser; -use coset::{iana::CoapContentFormat, CborSerializable}; +use coset::{iana::CoapContentFormat, AsCborValue, CborSerializable}; use ed25519_dalek::{ ed25519::signature::Signer, pkcs8::{DecodePrivateKey, DecodePublicKey}, }; -use signed_doc::{DocumentRef, KidURI, Metadata, UuidV7}; +use signed_doc::{DocumentRef, KidUri, Metadata, UuidV7}; fn main() { if let Err(err) = Cli::parse().exec() { @@ -315,7 +315,7 @@ fn validate_cose( "COSE missing signature protected header `kid` field " ); - let kid = KidURI::try_from(key_id.as_ref())?; + let kid = KidUri::try_from(key_id.as_ref())?; println!("Signature Key ID: {kid}"); let data_to_sign = cose.tbs_data(&[], sign); let signature_bytes = sign.signature.as_slice().try_into().map_err(|_| { diff --git a/rust/signed_doc/src/lib.rs b/rust/signed_doc/src/lib.rs index e31d3212016..ef46e2e0fe5 100644 --- a/rust/signed_doc/src/lib.rs +++ b/rust/signed_doc/src/lib.rs @@ -13,7 +13,7 @@ mod signature; pub use metadata::{DocumentRef, Metadata, UuidV7}; use payload::JsonContent; -pub use signature::KidURI; +pub use signature::KidUri; /// Inner type that holds the Catalyst Signed Document with parsing errors. #[derive(Default)] diff --git a/rust/signed_doc/src/signature/mod.rs b/rust/signed_doc/src/signature/mod.rs index d48e636d2b9..9ce4d711914 100644 --- a/rust/signed_doc/src/signature/mod.rs +++ b/rust/signed_doc/src/signature/mod.rs @@ -1,4 +1,2 @@ //! Catalyst Signed Document COSE Signature information. -mod kid; - -pub use kid::KidURI; +pub use catalyst_types::kid_uri::KidUri; From a6e33d62ded521b4d797d2fbc99ef6ddd95683a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Rosales?= Date: Wed, 8 Jan 2025 00:31:49 -0600 Subject: [PATCH 36/71] wip(rust/signed_doc): use catalyst-types crate for uuids --- rust/signed_doc/src/metadata/mod.rs | 3 +- rust/signed_doc/src/metadata/uuid_type/mod.rs | 27 -------- .../src/metadata/uuid_type/uuid_v4.rs | 62 ----------------- .../src/metadata/uuid_type/uuid_v7.rs | 69 ------------------- 4 files changed, 1 insertion(+), 160 deletions(-) delete mode 100644 rust/signed_doc/src/metadata/uuid_type/mod.rs delete mode 100644 rust/signed_doc/src/metadata/uuid_type/uuid_v4.rs delete mode 100644 rust/signed_doc/src/metadata/uuid_type/uuid_v7.rs diff --git a/rust/signed_doc/src/metadata/mod.rs b/rust/signed_doc/src/metadata/mod.rs index 3c4fda59c78..9b3dc339f28 100644 --- a/rust/signed_doc/src/metadata/mod.rs +++ b/rust/signed_doc/src/metadata/mod.rs @@ -7,15 +7,14 @@ mod document_id; mod document_ref; mod document_type; mod document_version; -mod uuid_type; +pub use catalyst_types::uuid::{V4 as UuidV4, V7 as UuidV7}; pub use content_encoding::ContentEncoding; pub use content_type::ContentType; pub use document_id::DocumentId; pub use document_ref::DocumentRef; pub use document_type::DocumentType; pub use document_version::DocumentVersion; -pub use uuid_type::{UuidV4, UuidV7}; /// Catalyst Signed Document Content Encoding Key. const CONTENT_ENCODING_KEY: &str = "Content-Encoding"; diff --git a/rust/signed_doc/src/metadata/uuid_type/mod.rs b/rust/signed_doc/src/metadata/uuid_type/mod.rs deleted file mode 100644 index 5aac63aa5ab..00000000000 --- a/rust/signed_doc/src/metadata/uuid_type/mod.rs +++ /dev/null @@ -1,27 +0,0 @@ -//! `UUID` types. - -mod uuid_v4; -mod uuid_v7; - -pub use uuid_v4::UuidV4; -pub use uuid_v7::UuidV7; - -/// Invalid Doc Type UUID -pub(crate) const INVALID_UUID: uuid::Uuid = uuid::Uuid::from_bytes([0x00; 16]); - -/// CBOR tag for UUID content. -const UUID_CBOR_TAG: u64 = 37; - -/// Decode `CBOR` encoded `UUID`. -pub(crate) fn decode_cbor_uuid(val: &coset::cbor::Value) -> anyhow::Result { - 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) -} diff --git a/rust/signed_doc/src/metadata/uuid_type/uuid_v4.rs b/rust/signed_doc/src/metadata/uuid_type/uuid_v4.rs deleted file mode 100644 index ef409e94bd8..00000000000 --- a/rust/signed_doc/src/metadata/uuid_type/uuid_v4.rs +++ /dev/null @@ -1,62 +0,0 @@ -//! `UUIDv4` Type. -use std::fmt::{Display, Formatter}; - -use super::{decode_cbor_uuid, INVALID_UUID}; - -/// Type representing a `UUIDv4`. -#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, serde::Deserialize)] -#[serde(from = "uuid::Uuid")] -pub struct UuidV4 { - /// UUID - uuid: uuid::Uuid, -} - -impl UuidV4 { - /// Version for `UUIDv4`. - const UUID_VERSION_NUMBER: usize = 4; - - /// Generates a zeroed out `UUIDv4` that can never be valid. - pub fn invalid() -> Self { - Self { uuid: INVALID_UUID } - } - - /// Returns the `uuid::Uuid` type. - #[must_use] - pub fn uuid(&self) -> uuid::Uuid { - self.uuid - } -} - -impl Display for UuidV4 { - fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { - write!(f, "{}", self.uuid) - } -} - -impl TryFrom<&coset::cbor::Value> for UuidV4 { - type Error = anyhow::Error; - - fn try_from(cbor_value: &coset::cbor::Value) -> Result { - match decode_cbor_uuid(cbor_value) { - Ok(uuid) => { - if uuid.get_version_num() == Self::UUID_VERSION_NUMBER { - Ok(Self { uuid }) - } else { - anyhow::bail!("UUID {uuid} is not `v{}`", Self::UUID_VERSION_NUMBER); - } - }, - Err(e) => { - anyhow::bail!("Invalid UUID. Error: {e}"); - }, - } - } -} - -/// Returns a `UUIDv4` from `uuid::Uuid`. -/// -/// NOTE: This does not guarantee that the `UUID` is valid. -impl From for UuidV4 { - fn from(uuid: uuid::Uuid) -> Self { - Self { uuid } - } -} diff --git a/rust/signed_doc/src/metadata/uuid_type/uuid_v7.rs b/rust/signed_doc/src/metadata/uuid_type/uuid_v7.rs deleted file mode 100644 index 7ef7a3a2446..00000000000 --- a/rust/signed_doc/src/metadata/uuid_type/uuid_v7.rs +++ /dev/null @@ -1,69 +0,0 @@ -//! `UUIDv7` Type. -use std::fmt::{Display, Formatter}; - -use super::{decode_cbor_uuid, INVALID_UUID}; - -/// Type representing a `UUIDv7`. -#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, serde::Deserialize)] -#[serde(from = "uuid::Uuid")] -pub struct UuidV7 { - /// UUID - uuid: uuid::Uuid, -} - -impl UuidV7 { - /// Version for `UUIDv7`. - const UUID_VERSION_NUMBER: usize = 7; - - /// Generates a zeroed out `UUIDv7` that can never be valid. - #[must_use] - pub fn invalid() -> Self { - Self { uuid: INVALID_UUID } - } - - /// Check if this is a valid `UUIDv7`. - #[must_use] - pub fn is_valid(&self) -> bool { - self.uuid != INVALID_UUID && self.uuid.get_version_num() == Self::UUID_VERSION_NUMBER - } - - /// Returns the `uuid::Uuid` type. - #[must_use] - pub fn uuid(&self) -> uuid::Uuid { - self.uuid - } -} - -impl Display for UuidV7 { - fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { - write!(f, "{}", self.uuid) - } -} - -impl TryFrom<&coset::cbor::Value> for UuidV7 { - type Error = anyhow::Error; - - fn try_from(cbor_value: &coset::cbor::Value) -> Result { - match decode_cbor_uuid(cbor_value) { - Ok(uuid) => { - if uuid.get_version_num() == Self::UUID_VERSION_NUMBER { - Ok(Self { uuid }) - } else { - anyhow::bail!("UUID {uuid} is not `v{}`", Self::UUID_VERSION_NUMBER); - } - }, - Err(e) => { - anyhow::bail!("Invalid UUID. Error: {e}"); - }, - } - } -} - -/// Returns a `UUIDv7` from `uuid::Uuid`. -/// -/// NOTE: This does not guarantee that the `UUID` is valid. -impl From for UuidV7 { - fn from(uuid: uuid::Uuid) -> Self { - Self { uuid } - } -} From 2fb1abf06313a946611e17ee3dc7af218ade8a0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Rosales?= Date: Wed, 8 Jan 2025 00:33:02 -0600 Subject: [PATCH 37/71] wip(rust/signed_doc): use catalyst-types crate for kid uri --- .../signed_doc/src/signature/kid/authority.rs | 37 --- rust/signed_doc/src/signature/kid/errors.rs | 43 ---- .../src/signature/kid/key_rotation.rs | 42 ---- rust/signed_doc/src/signature/kid/mod.rs | 231 ------------------ rust/signed_doc/src/signature/kid/role0_pk.rs | 40 --- .../src/signature/kid/role_index.rs | 44 ---- 6 files changed, 437 deletions(-) delete mode 100644 rust/signed_doc/src/signature/kid/authority.rs delete mode 100644 rust/signed_doc/src/signature/kid/errors.rs delete mode 100644 rust/signed_doc/src/signature/kid/key_rotation.rs delete mode 100644 rust/signed_doc/src/signature/kid/mod.rs delete mode 100644 rust/signed_doc/src/signature/kid/role0_pk.rs delete mode 100644 rust/signed_doc/src/signature/kid/role_index.rs diff --git a/rust/signed_doc/src/signature/kid/authority.rs b/rust/signed_doc/src/signature/kid/authority.rs deleted file mode 100644 index e2ab0cb182e..00000000000 --- a/rust/signed_doc/src/signature/kid/authority.rs +++ /dev/null @@ -1,37 +0,0 @@ -//! COSE Signature Protected Header `kid` URI Authority. - -use std::{ - fmt::{Display, Formatter}, - str::FromStr, -}; - -/// URI Authority -#[derive(Debug, Clone)] -pub enum Authority { - /// Cardano Blockchain - Cardano, - /// Midnight Blockchain - Midnight, -} - -impl FromStr for Authority { - type Err = anyhow::Error; - - fn from_str(s: &str) -> Result { - match s { - "cardano" => Ok(Authority::Cardano), - "midnight" => Ok(Authority::Midnight), - _ => Err(anyhow::anyhow!("Unknown Authority: {s}")), - } - } -} - -impl Display for Authority { - fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { - let authority = match self { - Self::Cardano => "cardano", - Self::Midnight => "midnight", - }; - write!(f, "{authority}") - } -} diff --git a/rust/signed_doc/src/signature/kid/errors.rs b/rust/signed_doc/src/signature/kid/errors.rs deleted file mode 100644 index f7aa25f230e..00000000000 --- a/rust/signed_doc/src/signature/kid/errors.rs +++ /dev/null @@ -1,43 +0,0 @@ -//! Errors returned by this type - -use thiserror::Error; - -use super::{key_rotation::KeyRotationError, role_index::RoleIndexError}; - -/// Errors that can occur when parsing a `KidURI` -#[derive(Error, Debug)] -pub enum KidURIError { - /// Invalid KID URI - #[error("Invalid URI")] - InvalidURI(#[from] fluent_uri::error::ParseError), - /// Invalid Scheme, not a KID URI - #[error("Invalid Scheme, not a KID URI")] - InvalidScheme, - /// Network not defined in URI - #[error("No defined Network")] - NoDefinedNetwork, - /// Path of URI is invalid - #[error("Invalid Path")] - InvalidPath, - /// Role 0 Key in path is invalid - #[error("Invalid Role 0 Key")] - InvalidRole0Key, - /// Role 0 Key in path is not encoded correctly - #[error("Invalid Role 0 Key Encoding")] - InvalidRole0KeyEncoding(#[from] base64_url::base64::DecodeError), - /// Role Index is invalid - #[error("Invalid Role")] - InvalidRole, - /// Role Index is not encoded correctly - #[error("Invalid Role Index")] - InvalidRoleIndex(#[from] RoleIndexError), - /// Role Key Rotation is invalid - #[error("Invalid Rotation")] - InvalidRotation, - /// Role Key Rotation is not encoded correctly - #[error("Invalid Rotation Value")] - InvalidRotationValue(#[from] KeyRotationError), - /// Encryption key Identifier Fragment is not valid - #[error("Invalid Encryption Key Fragment")] - InvalidEncryptionKeyFragment, -} diff --git a/rust/signed_doc/src/signature/kid/key_rotation.rs b/rust/signed_doc/src/signature/kid/key_rotation.rs deleted file mode 100644 index e41fcb046e1..00000000000 --- a/rust/signed_doc/src/signature/kid/key_rotation.rs +++ /dev/null @@ -1,42 +0,0 @@ -//! COSE Signature Protected Header `kid` Role0 Key Version. - -use std::{ - fmt::{Display, Formatter}, - num::ParseIntError, - str::FromStr, -}; - -use thiserror::Error; - -/// Errors from parsing the `KeyRotation` -#[derive(Error, Debug)] -#[allow(clippy::module_name_repetitions)] -pub enum KeyRotationError { - /// Key Rotation could not be parsed from a string - #[error("Invalid Role Key Rotation")] - InvalidRole(#[from] ParseIntError), -} - -/// Rotation count of the Role Key. -#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct KeyRotation(u16); - -impl From for KeyRotation { - fn from(value: u16) -> Self { - Self(value) - } -} - -impl FromStr for KeyRotation { - type Err = KeyRotationError; - - fn from_str(s: &str) -> Result { - Ok(Self(s.parse::()?)) - } -} - -impl Display for KeyRotation { - fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { - write!(f, "{}", self.0) - } -} diff --git a/rust/signed_doc/src/signature/kid/mod.rs b/rust/signed_doc/src/signature/kid/mod.rs deleted file mode 100644 index 71cc9380789..00000000000 --- a/rust/signed_doc/src/signature/kid/mod.rs +++ /dev/null @@ -1,231 +0,0 @@ -//! COSE Signature Protected Header `kid`. -mod authority; -mod errors; -mod key_rotation; -mod role0_pk; -mod role_index; - -use std::{ - fmt::{Display, Formatter}, - str::FromStr, -}; - -use ed25519_dalek::VerifyingKey; -use fluent_uri::{ - component::Scheme, - encoding::{ - encoder::{Fragment, Path}, - EStr, - }, - Uri, -}; -use key_rotation::KeyRotation; -use role_index::RoleIndex; - -/// Catalyst Signed Document Key ID -/// -/// Key ID associated with a `COSE` Signature that is structured as a Universal Resource -/// Identifier (`URI`). -#[derive(Debug, Clone)] -#[allow(clippy::module_name_repetitions)] -pub struct KidURI { - /// Network - network: String, - /// Sub Network - subnet: Option, - /// Role0 Public Key. - role0_pk: VerifyingKey, - /// User Role specified for the current document. - role: RoleIndex, - /// Role Key Rotation count - rotation: KeyRotation, - /// Is this an Encryption Key - encryption: bool, -} - -impl KidURI { - /// Encryption Key Identifier Fragment - const ENCRYPTION_FRAGMENT: &EStr = EStr::new_or_panic("encrypt"); - /// URI scheme for Catalyst - const SCHEME: &Scheme = Scheme::new_or_panic("kid.catalyst-rbac"); - - /// Get the network the `KidURI` is referencing the registration to. - #[must_use] - pub fn network(&self) -> (String, Option) { - (self.network.clone(), self.subnet.clone()) - } - - /// Is the key a signature type key. - #[must_use] - pub fn is_signature_key(&self) -> bool { - !self.encryption - } - - /// Is the key an encryption type key. - #[must_use] - pub fn is_encryption_key(&self) -> bool { - self.encryption - } - - /// Get the Initial Role 0 Key of the registration - #[must_use] - pub fn role0_pk(&self) -> VerifyingKey { - self.role0_pk - } - - /// Get the role index and its rotation count - #[must_use] - pub fn role_and_rotation(&self) -> (RoleIndex, KeyRotation) { - (self.role, self.rotation) - } -} - -impl KidURI { - /// Create a new `KidURI` for a Signing Key - fn new( - network: &str, subnet: Option<&str>, role0_pk: VerifyingKey, role: RoleIndex, - rotation: KeyRotation, - ) -> Self { - Self { - network: network.to_string(), - subnet: subnet.map(str::to_string), - role0_pk, - role, - rotation, - encryption: false, - } - } - - /// Create a new `KidURI` for an Encryption Key - fn new_encryption( - network: &str, subnet: Option<&str>, role0_pk: VerifyingKey, role: RoleIndex, - rotation: KeyRotation, - ) -> Self { - let mut kid = Self::new(network, subnet, role0_pk, role, rotation); - kid.encryption = true; - kid - } -} - -impl FromStr for KidURI { - type Err = errors::KidURIError; - - fn from_str(s: &str) -> Result { - let uri = Uri::parse(s)?; - - // Check if its the correct scheme. - if uri.scheme() != KidURI::SCHEME { - return Err(errors::KidURIError::InvalidScheme); - } - - // Decode the network and subnet - let auth = uri - .authority() - .ok_or(errors::KidURIError::NoDefinedNetwork)?; - let network = auth.host(); - let subnet = auth.userinfo().map(std::string::ToString::to_string); - - let path: Vec<&EStr> = uri.path().split('/').collect(); - - // Can ONLY have 3 path components, no more and no less - // Less than 3 handled by errors below (4 because of leading `/` in path). - if path.len() > 4 { - return Err(errors::KidURIError::InvalidPath); - }; - - // Decode and validate the Role0 Public key from the path - let encoded_role0_key = path.get(1).ok_or(errors::KidURIError::InvalidRole0Key)?; - let decoded_role0_key = - base64_url::decode(encoded_role0_key.decode().into_string_lossy().as_ref())?; - let role0_pk = cardano_blockchain_types::conversion::vkey_from_bytes(&decoded_role0_key) - .or(Err(errors::KidURIError::InvalidRole0Key))?; - - // Decode and validate the Role Index from the path. - let encoded_role_index = path.get(2).ok_or(errors::KidURIError::InvalidRole)?; - let decoded_role_index = encoded_role_index.decode().into_string_lossy(); - let role_index = decoded_role_index.parse::()?; - - // Decode and validate the Rotation Value from the path. - let encoded_rotation = path.get(3).ok_or(errors::KidURIError::InvalidRotation)?; - let decoded_rotation = encoded_rotation.decode().into_string_lossy(); - let rotation = decoded_rotation.parse::()?; - - let kid = { - if uri.has_fragment() { - if uri.fragment() == Some(Self::ENCRYPTION_FRAGMENT) { - Self::new_encryption(network, subnet.as_deref(), role0_pk, role_index, rotation) - } else { - return Err(errors::KidURIError::InvalidEncryptionKeyFragment); - } - } else { - Self::new(network, subnet.as_deref(), role0_pk, role_index, rotation) - } - }; - - Ok(kid) - } -} - -impl Display for KidURI { - fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { - write!(f, "{}://", Self::SCHEME.as_str())?; - if let Some(subnet) = &self.subnet { - write!(f, "{subnet}@")?; - } - write!( - f, - "{}/{}/{}/{}", - self.network, - base64_url::encode(self.role0_pk.as_bytes()), - self.role, - self.rotation - )?; - if self.encryption { - write!(f, "#{}", Self::ENCRYPTION_FRAGMENT)?; - } - Ok(()) - } -} - -impl TryFrom<&[u8]> for KidURI { - type Error = errors::KidURIError; - - fn try_from(value: &[u8]) -> Result { - let kid_str = String::from_utf8_lossy(value); - KidURI::from_str(&kid_str) - } -} - -#[cfg(test)] -mod tests { - use ed25519_dalek::SigningKey; - use rand::rngs::OsRng; - - use super::KidURI; - - const KID_TEST_VECTOR: [&str; 5] = [ - "kid.catalyst-rbac://cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE/0/0", - "kid.catalyst-rbac://preprod@cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE/7/3", - "kid.catalyst-rbac://preprod@cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE/2/0#encrypt", - "kid.catalyst-rbac://midnight/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE/0/1", - "kid.catalyst-rbac://midnight/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE/2/1#encrypt" - ]; - - #[test] - fn test_kid_uri_from_str() { - for kid_string in KID_TEST_VECTOR { - let kid = kid_string.parse::().unwrap(); - assert_eq!(format!("{kid}"), kid_string); - } - } - - #[ignore] - #[test] - fn gen_pk() { - let mut csprng = OsRng; - let signing_key: SigningKey = SigningKey::generate(&mut csprng); - let vk = signing_key.verifying_key(); - let encoded_vk = base64_url::encode(vk.as_bytes()); - assert_eq!(encoded_vk, "1234"); - } -} diff --git a/rust/signed_doc/src/signature/kid/role0_pk.rs b/rust/signed_doc/src/signature/kid/role0_pk.rs deleted file mode 100644 index 4557807fea7..00000000000 --- a/rust/signed_doc/src/signature/kid/role0_pk.rs +++ /dev/null @@ -1,40 +0,0 @@ -//! COSE Signature Protected Header `kid` URI Role0 Public Key. - -use std::{ - fmt::{Display, Formatter}, - str::FromStr, -}; - -/// Role0 Public Key. -#[derive(Debug, Clone)] -pub struct Role0PublicKey([u8; 32]); - -impl FromStr for Role0PublicKey { - type Err = anyhow::Error; - - fn from_str(s: &str) -> Result { - let Some(role0_hex) = s.strip_prefix("0x") else { - anyhow::bail!("Role0 Public Key hex string must start with '0x': {}", s); - }; - let role0_key = hex::decode(role0_hex) - .map_err(|e| anyhow::anyhow!("Role0 Public Key is not a valid hex string: {}", e))?; - if role0_key.len() != 32 { - anyhow::bail!( - "Role0 Public Key must have 32 bytes: {role0_hex}, len: {}", - role0_key.len() - ); - } - let role0 = role0_key.try_into().map_err(|e| { - anyhow::anyhow!( - "Unable to read Role0 Public Key, this should never happen. Eror: {e:?}" - ) - })?; - Ok(Role0PublicKey(role0)) - } -} - -impl Display for Role0PublicKey { - fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { - write!(f, "0x{}", hex::encode(self.0)) - } -} diff --git a/rust/signed_doc/src/signature/kid/role_index.rs b/rust/signed_doc/src/signature/kid/role_index.rs deleted file mode 100644 index ddb2b2cc4e0..00000000000 --- a/rust/signed_doc/src/signature/kid/role_index.rs +++ /dev/null @@ -1,44 +0,0 @@ -//! COSE Signature Protected Header `kid` URI Catalyst User Role. - -use std::{ - fmt::{Display, Formatter}, - num::ParseIntError, - str::FromStr, -}; - -use thiserror::Error; - -/// Role Index parsing error -#[derive(Error, Debug)] -#[allow(clippy::module_name_repetitions)] -pub enum RoleIndexError { - /// Failed to parse the role index - #[error("Invalid Role Index")] - InvalidRole(#[from] ParseIntError), -} - -/// Project Catalyst User Role Index. -/// -/// -#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct RoleIndex(u16); - -impl From for RoleIndex { - fn from(value: u16) -> Self { - Self(value) - } -} - -impl FromStr for RoleIndex { - type Err = RoleIndexError; - - fn from_str(s: &str) -> Result { - Ok(Self(s.parse::()?)) - } -} - -impl Display for RoleIndex { - fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { - write!(f, "{}", self.0) - } -} From 1804fefa31a0d0d2c3d4ba0a7961340b7d2fc726 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Thu, 9 Jan 2025 11:11:01 +0200 Subject: [PATCH 38/71] remove redundant signatures field --- rust/signed_doc/src/lib.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/rust/signed_doc/src/lib.rs b/rust/signed_doc/src/lib.rs index ef46e2e0fe5..c00a9f36a0c 100644 --- a/rust/signed_doc/src/lib.rs +++ b/rust/signed_doc/src/lib.rs @@ -22,8 +22,6 @@ struct InnerCatalystSignedDocument { metadata: Metadata, /// Document Payload viewed as JSON Content payload: JsonContent, - /// Signatures - signatures: Vec, /// Raw COSE Sign data cose_sign: coset::CoseSign, /// Raw COSE Sign bytes @@ -46,7 +44,7 @@ impl Display for CatalystSignedDocument { writeln!(f, "{}", self.inner.metadata)?; writeln!(f, "{:#?}\n", self.inner.payload)?; writeln!(f, "Signature Information [")?; - for signature in &self.inner.signatures { + for signature in &self.inner.cose_sign.signatures { writeln!( f, " {} 0x{:#}", @@ -93,11 +91,9 @@ impl TryFrom> for CatalystSignedDocument { content_errors.push("COSE payload is empty".to_string()); }; - let signatures = cose.signatures.clone(); let inner = InnerCatalystSignedDocument { metadata, payload, - signatures, cose_sign: cose, cose_bytes, content_errors, From 6af9ab52c8077e7ca813002a6da7326d416e1db6 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Thu, 9 Jan 2025 11:11:26 +0200 Subject: [PATCH 39/71] update cose_protected_header_find function --- rust/signed_doc/src/metadata/mod.rs | 117 +++++++++++++++------------- 1 file changed, 63 insertions(+), 54 deletions(-) diff --git a/rust/signed_doc/src/metadata/mod.rs b/rust/signed_doc/src/metadata/mod.rs index 9b3dc339f28..bf17819698f 100644 --- a/rust/signed_doc/src/metadata/mod.rs +++ b/rust/signed_doc/src/metadata/mod.rs @@ -39,17 +39,17 @@ pub struct Metadata { content_encoding: Option, /// Additional Metadata Fields. #[serde(flatten)] - extra: Fields, + extra: AdditionalFields, /// Metadata Content Errors #[serde(skip)] content_errors: Vec, } -/// Optional Metadata Fields. +/// Additional Metadata Fields. /// /// These values are extracted from the COSE Sign protected header labels. #[derive(Default, Debug, serde::Deserialize)] -struct Fields { +struct AdditionalFields { /// Reference to the latest document. #[serde(rename = "ref")] doc_ref: Option, @@ -158,7 +158,7 @@ impl Default for Metadata { ver: DocumentVersion::invalid(), content_type: ContentType::default(), content_encoding: None, - extra: Fields::default(), + extra: AdditionalFields::default(), content_errors: Vec::new(), } } @@ -185,46 +185,45 @@ impl From<&coset::ProtectedHeader> for Metadata { ); }, } - match protected.header.rest.iter().find(|(key, _)| { - if let coset::Label::Text(label) = key { - label.eq_ignore_ascii_case(CONTENT_ENCODING_KEY) - } else { - false + + if let Some(value) = cose_protected_header_find( + protected, + |key| matches!(key, coset::Label::Text(label) if label.eq_ignore_ascii_case(CONTENT_ENCODING_KEY)), + ) { + match ContentEncoding::try_from(value) { + Ok(encoding) => { + metadata.content_encoding = Some(encoding); + }, + Err(e) => { + errors.push(format!("Invalid Document Content Encoding: {e}")); + }, } + } else { + errors.push(format!( + "Invalid COSE document protected header '{CONTENT_ENCODING_KEY}' is missing" + )); + } + + if let Some(doc_type) = cose_protected_header_find(protected, |key| { + key == &coset::Label::Text("type".to_string()) }) { - Some((_key, value)) => { - match ContentEncoding::try_from(value) { - Ok(encoding) => { - metadata.content_encoding = Some(encoding); - }, - Err(e) => { - errors.push(format!("Invalid Document Content Encoding: {e}")); - }, - } - }, - None => { - errors.push(format!( - "Invalid COSE document protected header '{CONTENT_ENCODING_KEY}' is missing" - )); - }, + match UuidV4::try_from(doc_type) { + Ok(doc_type_uuid) => { + metadata.doc_type = doc_type_uuid.into(); + }, + Err(e) => { + errors.push(format!("Document `type` is invalid: {e}")); + }, + } + } else { + errors.push("Invalid COSE protected header, missing `type` field".to_string()); } - match cose_protected_header_find(protected, "type") { - Some(doc_type) => { - match UuidV4::try_from(&doc_type) { - Ok(doc_type_uuid) => { - metadata.doc_type = doc_type_uuid.into(); - }, - Err(e) => { - errors.push(format!("Document `type` is invalid: {e}")); - }, - } - }, - None => errors.push("Invalid COSE protected header, missing `type` field".to_string()), - }; - match cose_protected_header_find(protected, "id") { + match cose_protected_header_find(protected, |key| { + key == &coset::Label::Text("id".to_string()) + }) { Some(doc_id) => { - match UuidV7::try_from(&doc_id) { + match UuidV7::try_from(doc_id) { Ok(doc_id_uuid) => { metadata.id = doc_id_uuid.into(); }, @@ -236,9 +235,11 @@ impl From<&coset::ProtectedHeader> for Metadata { None => errors.push("Invalid COSE protected header, missing `id` field".to_string()), }; - match cose_protected_header_find(protected, "ver") { + match cose_protected_header_find(protected, |key| { + key == &coset::Label::Text("ver".to_string()) + }) { Some(doc_ver) => { - match UuidV7::try_from(&doc_ver) { + match UuidV7::try_from(doc_ver) { Ok(doc_ver_uuid) => { if doc_ver_uuid.uuid() < metadata.id.uuid() { errors.push(format!( @@ -258,8 +259,10 @@ impl From<&coset::ProtectedHeader> for Metadata { None => errors.push("Invalid COSE protected header, missing `ver` field".to_string()), } - if let Some(cbor_doc_ref) = cose_protected_header_find(protected, "ref") { - match DocumentRef::try_from(&cbor_doc_ref) { + if let Some(cbor_doc_ref) = cose_protected_header_find(protected, |key| { + key == &coset::Label::Text("ref".to_string()) + }) { + match DocumentRef::try_from(cbor_doc_ref) { Ok(doc_ref) => { metadata.extra.doc_ref = Some(doc_ref); }, @@ -271,8 +274,10 @@ impl From<&coset::ProtectedHeader> for Metadata { } } - if let Some(cbor_doc_template) = cose_protected_header_find(protected, "template") { - match DocumentRef::try_from(&cbor_doc_template) { + if let Some(cbor_doc_template) = cose_protected_header_find(protected, |key| { + key == &coset::Label::Text("template".to_string()) + }) { + match DocumentRef::try_from(cbor_doc_template) { Ok(doc_template) => { metadata.extra.template = Some(doc_template); }, @@ -284,8 +289,10 @@ impl From<&coset::ProtectedHeader> for Metadata { } } - if let Some(cbor_doc_reply) = cose_protected_header_find(protected, "reply") { - match DocumentRef::try_from(&cbor_doc_reply) { + if let Some(cbor_doc_reply) = cose_protected_header_find(protected, |key| { + key == &coset::Label::Text("reply".to_string()) + }) { + match DocumentRef::try_from(cbor_doc_reply) { Ok(doc_reply) => { metadata.extra.reply = Some(doc_reply); }, @@ -297,8 +304,10 @@ impl From<&coset::ProtectedHeader> for Metadata { } } - if let Some(cbor_doc_section) = cose_protected_header_find(protected, "section") { - match cbor_doc_section.into_text() { + if let Some(cbor_doc_section) = cose_protected_header_find(protected, |key| { + key == &coset::Label::Text("section".to_string()) + }) { + match cbor_doc_section.clone().into_text() { Ok(doc_section) => { metadata.extra.section = Some(doc_section); }, @@ -314,14 +323,14 @@ impl From<&coset::ProtectedHeader> for Metadata { } } -/// Find a value for a given key in the protected header. +/// Find a value for a predicate in the protected header. fn cose_protected_header_find( - protected: &coset::ProtectedHeader, rest_key: &str, -) -> Option { + protected: &coset::ProtectedHeader, mut predicate: impl FnMut(&coset::Label) -> bool, +) -> Option<&coset::cbor::Value> { protected .header .rest .iter() - .find(|(key, _)| key == &coset::Label::Text(rest_key.to_string())) - .map(|(_, value)| value.clone()) + .find(|(key, _)| predicate(key)) + .map(|(_, value)| value) } From e06aa2fcb821f5ea8ca4028d8198a8c7abc9b929 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Thu, 9 Jan 2025 11:17:35 +0200 Subject: [PATCH 40/71] fix DocumentId, DocumentRef structs --- rust/signed_doc/examples/mk_signed_doc.rs | 6 +++--- rust/signed_doc/src/metadata/document_id.rs | 15 +++++---------- rust/signed_doc/src/metadata/document_ref.rs | 10 +++++++--- 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/rust/signed_doc/examples/mk_signed_doc.rs b/rust/signed_doc/examples/mk_signed_doc.rs index 91cd64b8625..88fea415ad8 100644 --- a/rust/signed_doc/examples/mk_signed_doc.rs +++ b/rust/signed_doc/examples/mk_signed_doc.rs @@ -9,7 +9,7 @@ use std::{ }; use clap::Parser; -use coset::{iana::CoapContentFormat, AsCborValue, CborSerializable}; +use coset::{iana::CoapContentFormat, CborSerializable}; use ed25519_dalek::{ ed25519::signature::Signer, pkcs8::{DecodePrivateKey, DecodePublicKey}, @@ -84,7 +84,7 @@ fn decode_cbor_uuid(val: &coset::cbor::Value) -> anyhow::Result { fn encode_cbor_document_ref(doc_ref: &DocumentRef) -> coset::cbor::Value { match doc_ref { DocumentRef::Latest { id } => encode_cbor_uuid(&id.uuid()), - DocumentRef::WithVer(id, ver) => { + DocumentRef::WithVer { id, ver } => { coset::cbor::Value::Array(vec![ encode_cbor_uuid(&id.uuid()), encode_cbor_uuid(&ver.uuid()), @@ -104,7 +104,7 @@ fn decode_cbor_document_ref(val: &coset::cbor::Value) -> anyhow::Result Self { - Self { - uuid: UuidV7::invalid(), - } + Self(UuidV7::invalid()) } /// Returns the `uuid::Uuid` type. #[must_use] pub fn uuid(&self) -> uuid::Uuid { - self.uuid.uuid() + self.0.uuid() } } impl Display for DocumentId { fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { - write!(f, "{}", self.uuid) + write!(f, "{}", self.0) } } impl From for DocumentId { fn from(uuid: UuidV7) -> Self { - Self { uuid } + Self(uuid) } } diff --git a/rust/signed_doc/src/metadata/document_ref.rs b/rust/signed_doc/src/metadata/document_ref.rs index 114226b5405..d5d5c95a3ed 100644 --- a/rust/signed_doc/src/metadata/document_ref.rs +++ b/rust/signed_doc/src/metadata/document_ref.rs @@ -11,8 +11,12 @@ pub enum DocumentRef { id: UuidV7, }, /// Reference to the specific document version - /// Document ID UUID, Document Ver UUID - WithVer(UuidV7, UuidV7), + WithVer { + /// Document ID UUID, + id: UuidV7, + /// Document Ver UUID + ver: UuidV7, + }, } impl TryFrom<&coset::cbor::Value> for DocumentRef { @@ -36,7 +40,7 @@ impl TryFrom<&coset::cbor::Value> for DocumentRef { ver >= id, "Document Reference Version can never be smaller than its ID" ); - Ok(DocumentRef::WithVer(id, ver)) + Ok(DocumentRef::WithVer { id, ver }) } } } From b57b38fa11929680626842a4cc4e0a985d6a0aeb Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Thu, 9 Jan 2025 11:20:13 +0200 Subject: [PATCH 41/71] rename has_error to is_valid --- rust/signed_doc/src/lib.rs | 4 ++-- rust/signed_doc/src/metadata/mod.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/rust/signed_doc/src/lib.rs b/rust/signed_doc/src/lib.rs index c00a9f36a0c..d8f5c5f0651 100644 --- a/rust/signed_doc/src/lib.rs +++ b/rust/signed_doc/src/lib.rs @@ -74,7 +74,7 @@ impl TryFrom> for CatalystSignedDocument { let metadata = Metadata::from(&cose.protected); - if metadata.has_error() { + if metadata.is_valid() { content_errors.extend_from_slice(metadata.content_errors()); } @@ -109,7 +109,7 @@ impl CatalystSignedDocument { /// Are there any validation errors (as opposed to structural errors). #[must_use] - pub fn has_error(&self) -> bool { + pub fn is_valid(&self) -> bool { !self.inner.content_errors.is_empty() } diff --git a/rust/signed_doc/src/metadata/mod.rs b/rust/signed_doc/src/metadata/mod.rs index bf17819698f..3da374f4542 100644 --- a/rust/signed_doc/src/metadata/mod.rs +++ b/rust/signed_doc/src/metadata/mod.rs @@ -66,7 +66,7 @@ struct AdditionalFields { impl Metadata { /// Are there any validation errors (as opposed to structural errors). #[must_use] - pub fn has_error(&self) -> bool { + pub fn is_valid(&self) -> bool { !self.content_errors.is_empty() } From 8ee9a67746d10193efa46b23a497c640da47f626 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Thu, 9 Jan 2025 11:55:14 +0200 Subject: [PATCH 42/71] rename crate --- rust/signed_doc/Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rust/signed_doc/Cargo.toml b/rust/signed_doc/Cargo.toml index 0abe5e8e5ec..38c77d36b2d 100644 --- a/rust/signed_doc/Cargo.toml +++ b/rust/signed_doc/Cargo.toml @@ -1,6 +1,6 @@ [package] -name = "signed_doc" -version = "0.1.0" +name = "catalyst-signed-doc" +version = "0.0.1" edition.workspace = true authors.workspace = true homepage.workspace = true From bb0e29aa2a677823edcea7821d56ca867d0bc1dc Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Thu, 9 Jan 2025 18:57:46 +0200 Subject: [PATCH 43/71] remove cose_raw MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Joaquín Rosales --- rust/signed_doc/src/lib.rs | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/rust/signed_doc/src/lib.rs b/rust/signed_doc/src/lib.rs index d8f5c5f0651..b4657b69f0b 100644 --- a/rust/signed_doc/src/lib.rs +++ b/rust/signed_doc/src/lib.rs @@ -5,7 +5,7 @@ use std::{ sync::Arc, }; -use coset::{CborSerializable, TaggedCborSerializable}; +use coset::CborSerializable; mod metadata; mod payload; @@ -24,8 +24,6 @@ struct InnerCatalystSignedDocument { payload: JsonContent, /// Raw COSE Sign data cose_sign: coset::CoseSign, - /// Raw COSE Sign bytes - cose_bytes: Vec, /// Content Errors found when parsing the Document content_errors: Vec, } @@ -66,13 +64,12 @@ impl TryFrom> for CatalystSignedDocument { fn try_from(cose_bytes: Vec) -> Result { // Try reading as a tagged COSE SIGN, otherwise try reading as untagged. - let cose = coset::CoseSign::from_tagged_slice(&cose_bytes) - .or(coset::CoseSign::from_slice(&cose_bytes)) + let cose_sign = coset::CoseSign::from_slice(&cose_bytes) .map_err(|e| anyhow::anyhow!("Invalid COSE Sign document: {e}"))?; let mut content_errors = Vec::new(); - let metadata = Metadata::from(&cose.protected); + let metadata = Metadata::from(&cose_sign.protected); if metadata.is_valid() { content_errors.extend_from_slice(metadata.content_errors()); @@ -80,7 +77,7 @@ impl TryFrom> for CatalystSignedDocument { let mut payload = JsonContent::default(); - if let Some(bytes) = &cose.payload { + if let Some(bytes) = &cose_sign.payload { match JsonContent::try_from((bytes, metadata.content_encoding())) { Ok(c) => payload = c, Err(e) => { @@ -94,8 +91,7 @@ impl TryFrom> for CatalystSignedDocument { let inner = InnerCatalystSignedDocument { metadata, payload, - cose_sign: cose, - cose_bytes, + cose_sign, content_errors, }; Ok(CatalystSignedDocument { @@ -154,10 +150,4 @@ impl CatalystSignedDocument { pub fn doc_section(&self) -> Option { self.inner.metadata.doc_section() } - - /// Return Raw COSE SIGN bytes. - #[must_use] - pub fn cose_sign_bytes(&self) -> &[u8] { - self.inner.cose_bytes.as_ref() - } } From 782c7b9d2f9aa2286cf5f9b8834cdf6318b5c744 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Thu, 9 Jan 2025 19:59:29 +0200 Subject: [PATCH 44/71] move additional_fields into another mod --- rust/signed_doc/src/lib.rs | 6 +- .../src/metadata/additional_fields.rs | 96 ++++++++++++++ rust/signed_doc/src/metadata/mod.rs | 123 ++++-------------- 3 files changed, 125 insertions(+), 100 deletions(-) create mode 100644 rust/signed_doc/src/metadata/additional_fields.rs diff --git a/rust/signed_doc/src/lib.rs b/rust/signed_doc/src/lib.rs index b4657b69f0b..f989fb9e7ce 100644 --- a/rust/signed_doc/src/lib.rs +++ b/rust/signed_doc/src/lib.rs @@ -59,12 +59,12 @@ impl Display for CatalystSignedDocument { } } -impl TryFrom> for CatalystSignedDocument { +impl TryFrom<&[u8]> for CatalystSignedDocument { type Error = anyhow::Error; - fn try_from(cose_bytes: Vec) -> Result { + fn try_from(cose_bytes: &[u8]) -> Result { // Try reading as a tagged COSE SIGN, otherwise try reading as untagged. - let cose_sign = coset::CoseSign::from_slice(&cose_bytes) + let cose_sign = coset::CoseSign::from_slice(cose_bytes) .map_err(|e| anyhow::anyhow!("Invalid COSE Sign document: {e}"))?; let mut content_errors = Vec::new(); diff --git a/rust/signed_doc/src/metadata/additional_fields.rs b/rust/signed_doc/src/metadata/additional_fields.rs new file mode 100644 index 00000000000..c09b36e3142 --- /dev/null +++ b/rust/signed_doc/src/metadata/additional_fields.rs @@ -0,0 +1,96 @@ +//! Catalyst Signed Document Additional Fields. + +use anyhow::anyhow; + +use super::{cose_protected_header_find, DocumentRef}; + +/// Additional Metadata Fields. +/// +/// These values are extracted from the COSE Sign protected header labels. +#[derive(Default, Debug, serde::Deserialize)] +pub(super) struct AdditionalFields { + /// Reference to the latest document. + #[serde(rename = "ref")] + pub(super) doc_ref: Option, + /// Reference to the document template. + pub(super) template: Option, + /// Reference to the document reply. + pub(super) reply: Option, + /// Reference to the document section. + pub(super) section: Option, +} + +impl TryFrom<&coset::ProtectedHeader> for AdditionalFields { + type Error = Vec; + + fn try_from(protected: &coset::ProtectedHeader) -> Result { + let mut extra = AdditionalFields::default(); + let mut errors = Vec::new(); + + if let Some(cbor_doc_ref) = cose_protected_header_find(protected, |key| { + key == &coset::Label::Text("ref".to_string()) + }) { + match DocumentRef::try_from(cbor_doc_ref) { + Ok(doc_ref) => { + extra.doc_ref = Some(doc_ref); + }, + Err(e) => { + errors.push(anyhow!( + "Invalid COSE protected header `ref` field, err: {e}" + )); + }, + } + } + + if let Some(cbor_doc_template) = cose_protected_header_find(protected, |key| { + key == &coset::Label::Text("template".to_string()) + }) { + match DocumentRef::try_from(cbor_doc_template) { + Ok(doc_template) => { + extra.template = Some(doc_template); + }, + Err(e) => { + errors.push(anyhow!( + "Invalid COSE protected header `template` field, err: {e}" + )); + }, + } + } + + if let Some(cbor_doc_reply) = cose_protected_header_find(protected, |key| { + key == &coset::Label::Text("reply".to_string()) + }) { + match DocumentRef::try_from(cbor_doc_reply) { + Ok(doc_reply) => { + extra.reply = Some(doc_reply); + }, + Err(e) => { + errors.push(anyhow!( + "Invalid COSE protected header `reply` field, err: {e}" + )); + }, + } + } + + if let Some(cbor_doc_section) = cose_protected_header_find(protected, |key| { + key == &coset::Label::Text("section".to_string()) + }) { + match cbor_doc_section.clone().into_text() { + Ok(doc_section) => { + extra.section = Some(doc_section); + }, + Err(e) => { + errors.push(anyhow!( + "Invalid COSE protected header `section` field, err: {e:?}" + )); + }, + } + } + + if errors.is_empty() { + Ok(extra) + } else { + Err(errors) + } + } +} diff --git a/rust/signed_doc/src/metadata/mod.rs b/rust/signed_doc/src/metadata/mod.rs index 3da374f4542..e6609aeb395 100644 --- a/rust/signed_doc/src/metadata/mod.rs +++ b/rust/signed_doc/src/metadata/mod.rs @@ -1,6 +1,7 @@ //! Catalyst Signed Document Metadata. use std::fmt::{Display, Formatter}; +mod additional_fields; mod content_encoding; mod content_type; mod document_id; @@ -8,6 +9,8 @@ mod document_ref; mod document_type; mod document_version; +use additional_fields::AdditionalFields; +use anyhow::anyhow; pub use catalyst_types::uuid::{V4 as UuidV4, V7 as UuidV7}; pub use content_encoding::ContentEncoding; pub use content_type::ContentType; @@ -45,24 +48,6 @@ pub struct Metadata { content_errors: Vec, } -/// Additional Metadata Fields. -/// -/// These values are extracted from the COSE Sign protected header labels. -#[derive(Default, Debug, serde::Deserialize)] -struct AdditionalFields { - /// Reference to the latest document. - #[serde(rename = "ref")] - doc_ref: Option, - /// Hash of the referenced document bytes. - ref_hash: Option>, - /// Reference to the document template. - template: Option, - /// Reference to the document reply. - reply: Option, - /// Reference to the document section. - section: Option, -} - impl Metadata { /// Are there any validation errors (as opposed to structural errors). #[must_use] @@ -100,12 +85,6 @@ impl Metadata { self.content_encoding } - /// Return Last Document Reference `Option>`. - #[must_use] - pub fn doc_ref_hash(&self) -> Option> { - self.extra.ref_hash.clone() - } - /// Return Last Document Reference `Option`. #[must_use] pub fn doc_ref(&self) -> Option { @@ -165,7 +144,6 @@ impl Default for Metadata { } impl From<&coset::ProtectedHeader> for Metadata { - #[allow(clippy::too_many_lines)] fn from(protected: &coset::ProtectedHeader) -> Self { let mut metadata = Metadata::default(); let mut errors = Vec::new(); @@ -175,14 +153,14 @@ impl From<&coset::ProtectedHeader> for Metadata { match ContentType::try_from(iana_content_type) { Ok(content_type) => metadata.content_type = content_type, Err(e) => { - errors.push(format!("Invalid Document Content-Type: {e}")); + errors.push(anyhow!("Invalid Document Content-Type: {e}")); }, } }, None => { - errors.push( - "COSE document protected header `content-type` field is missing".to_string(), - ); + errors.push(anyhow!( + "COSE document protected header `content-type` field is missing" + )); }, } @@ -195,11 +173,11 @@ impl From<&coset::ProtectedHeader> for Metadata { metadata.content_encoding = Some(encoding); }, Err(e) => { - errors.push(format!("Invalid Document Content Encoding: {e}")); + errors.push(anyhow!("Invalid Document Content Encoding: {e}")); }, } } else { - errors.push(format!( + errors.push(anyhow!( "Invalid COSE document protected header '{CONTENT_ENCODING_KEY}' is missing" )); } @@ -212,11 +190,13 @@ impl From<&coset::ProtectedHeader> for Metadata { metadata.doc_type = doc_type_uuid.into(); }, Err(e) => { - errors.push(format!("Document `type` is invalid: {e}")); + errors.push(anyhow!("Document `type` is invalid: {e}")); }, } } else { - errors.push("Invalid COSE protected header, missing `type` field".to_string()); + errors.push(anyhow!( + "Invalid COSE protected header, missing `type` field" + )); } match cose_protected_header_find(protected, |key| { @@ -228,11 +208,11 @@ impl From<&coset::ProtectedHeader> for Metadata { metadata.id = doc_id_uuid.into(); }, Err(e) => { - errors.push(format!("Document `id` is invalid: {e}")); + errors.push(anyhow!("Document `id` is invalid: {e}")); }, } }, - None => errors.push("Invalid COSE protected header, missing `id` field".to_string()), + None => errors.push(anyhow!("Invalid COSE protected header, missing `id` field")), }; match cose_protected_header_find(protected, |key| { @@ -242,7 +222,7 @@ impl From<&coset::ProtectedHeader> for Metadata { match UuidV7::try_from(doc_ver) { Ok(doc_ver_uuid) => { if doc_ver_uuid.uuid() < metadata.id.uuid() { - errors.push(format!( + errors.push(anyhow!( "Document Version {doc_ver_uuid} cannot be smaller than Document ID {}", metadata.id )); } else { @@ -250,75 +230,24 @@ impl From<&coset::ProtectedHeader> for Metadata { } }, Err(e) => { - errors.push(format!( + errors.push(anyhow!( "Invalid COSE protected header `ver` field, err: {e}" )); }, } }, - None => errors.push("Invalid COSE protected header, missing `ver` field".to_string()), - } - - if let Some(cbor_doc_ref) = cose_protected_header_find(protected, |key| { - key == &coset::Label::Text("ref".to_string()) - }) { - match DocumentRef::try_from(cbor_doc_ref) { - Ok(doc_ref) => { - metadata.extra.doc_ref = Some(doc_ref); - }, - Err(e) => { - errors.push(format!( - "Invalid COSE protected header `ref` field, err: {e}" - )); - }, - } - } - - if let Some(cbor_doc_template) = cose_protected_header_find(protected, |key| { - key == &coset::Label::Text("template".to_string()) - }) { - match DocumentRef::try_from(cbor_doc_template) { - Ok(doc_template) => { - metadata.extra.template = Some(doc_template); - }, - Err(e) => { - errors.push(format!( - "Invalid COSE protected header `template` field, err: {e}" - )); - }, - } + None => { + errors.push(anyhow!( + "Invalid COSE protected header, missing `ver` field" + )); + }, } - if let Some(cbor_doc_reply) = cose_protected_header_find(protected, |key| { - key == &coset::Label::Text("reply".to_string()) - }) { - match DocumentRef::try_from(cbor_doc_reply) { - Ok(doc_reply) => { - metadata.extra.reply = Some(doc_reply); - }, - Err(e) => { - errors.push(format!( - "Invalid COSE protected header `reply` field, err: {e}" - )); - }, - } - } + match AdditionalFields::try_from(protected) { + Ok(extra) => metadata.extra = extra, + Err(e) => errors.extend(e), + }; - if let Some(cbor_doc_section) = cose_protected_header_find(protected, |key| { - key == &coset::Label::Text("section".to_string()) - }) { - match cbor_doc_section.clone().into_text() { - Ok(doc_section) => { - metadata.extra.section = Some(doc_section); - }, - Err(e) => { - errors.push(format!( - "Invalid COSE protected header `section` field, err: {e:?}" - )); - }, - } - } - metadata.content_errors = errors; metadata } } From 5c74471afaa724a4b4bd88862a14a36b4ebc190a Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Thu, 9 Jan 2025 20:05:58 +0200 Subject: [PATCH 45/71] change Try to TryFrom --- rust/signed_doc/src/lib.rs | 34 +++++++---------------------- rust/signed_doc/src/metadata/mod.rs | 13 ++++++++--- 2 files changed, 18 insertions(+), 29 deletions(-) diff --git a/rust/signed_doc/src/lib.rs b/rust/signed_doc/src/lib.rs index f989fb9e7ce..8c3d3d2afb6 100644 --- a/rust/signed_doc/src/lib.rs +++ b/rust/signed_doc/src/lib.rs @@ -5,6 +5,7 @@ use std::{ sync::Arc, }; +use anyhow::anyhow; use coset::CborSerializable; mod metadata; @@ -24,8 +25,6 @@ struct InnerCatalystSignedDocument { payload: JsonContent, /// Raw COSE Sign data cose_sign: coset::CoseSign, - /// Content Errors found when parsing the Document - content_errors: Vec, } /// Keep all the contents private. @@ -50,49 +49,38 @@ impl Display for CatalystSignedDocument { hex::encode(signature.signature.as_slice()) )?; } - writeln!(f, "]\n")?; - writeln!(f, "Content Errors [")?; - for error in &self.inner.content_errors { - writeln!(f, " {error:#}")?; - } - writeln!(f, "]") + writeln!(f, "]\n") } } impl TryFrom<&[u8]> for CatalystSignedDocument { - type Error = anyhow::Error; + type Error = Vec; fn try_from(cose_bytes: &[u8]) -> Result { // Try reading as a tagged COSE SIGN, otherwise try reading as untagged. let cose_sign = coset::CoseSign::from_slice(cose_bytes) - .map_err(|e| anyhow::anyhow!("Invalid COSE Sign document: {e}"))?; - - let mut content_errors = Vec::new(); + .map_err(|e| vec![anyhow::anyhow!("Invalid COSE Sign document: {e}")])?; - let metadata = Metadata::from(&cose_sign.protected); - - if metadata.is_valid() { - content_errors.extend_from_slice(metadata.content_errors()); - } + let metadata = Metadata::try_from(&cose_sign.protected)?; + let mut content_errors = Vec::new(); let mut payload = JsonContent::default(); if let Some(bytes) = &cose_sign.payload { match JsonContent::try_from((bytes, metadata.content_encoding())) { Ok(c) => payload = c, Err(e) => { - content_errors.push(format!("Invalid Payload: {e}")); + content_errors.push(anyhow!("Invalid Payload: {e}")); }, } } else { - content_errors.push("COSE payload is empty".to_string()); + content_errors.push(anyhow!("COSE payload is empty")); }; let inner = InnerCatalystSignedDocument { metadata, payload, cose_sign, - content_errors, }; Ok(CatalystSignedDocument { inner: Arc::new(inner), @@ -103,12 +91,6 @@ impl TryFrom<&[u8]> for CatalystSignedDocument { impl CatalystSignedDocument { // A bunch of getters to access the contents, or reason through the document, such as. - /// Are there any validation errors (as opposed to structural errors). - #[must_use] - pub fn is_valid(&self) -> bool { - !self.inner.content_errors.is_empty() - } - /// Return Document Type `UUIDv4`. #[must_use] pub fn doc_type(&self) -> uuid::Uuid { diff --git a/rust/signed_doc/src/metadata/mod.rs b/rust/signed_doc/src/metadata/mod.rs index e6609aeb395..433e6ddb97e 100644 --- a/rust/signed_doc/src/metadata/mod.rs +++ b/rust/signed_doc/src/metadata/mod.rs @@ -143,8 +143,11 @@ impl Default for Metadata { } } -impl From<&coset::ProtectedHeader> for Metadata { - fn from(protected: &coset::ProtectedHeader) -> Self { +impl TryFrom<&coset::ProtectedHeader> for Metadata { + type Error = Vec; + + #[allow(clippy::too_many_lines)] + fn try_from(protected: &coset::ProtectedHeader) -> Result { let mut metadata = Metadata::default(); let mut errors = Vec::new(); @@ -248,7 +251,11 @@ impl From<&coset::ProtectedHeader> for Metadata { Err(e) => errors.extend(e), }; - metadata + if errors.is_empty() { + Ok(metadata) + } else { + Err(errors) + } } } From 2b9282f44e86dbd0a5dc78e40e9c890ab06a0f70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Rosales?= Date: Thu, 9 Jan 2025 15:40:29 -0600 Subject: [PATCH 46/71] fix(rust/signed_doc): decode and validate signatures * WIP: implement temporary Error type that will be replaced with a ProblemReport --- rust/signed_doc/examples/cat-signed-doc.rs | 4 +- rust/signed_doc/examples/mk_signed_doc.rs | 2 +- rust/signed_doc/src/error.rs | 19 ++++++++ rust/signed_doc/src/lib.rs | 44 +++++++++++++++-- rust/signed_doc/src/metadata/mod.rs | 8 ++-- rust/signed_doc/src/payload/json.rs | 17 +++---- rust/signed_doc/src/signature/mod.rs | 55 ++++++++++++++++++++++ 7 files changed, 129 insertions(+), 20 deletions(-) create mode 100644 rust/signed_doc/src/error.rs diff --git a/rust/signed_doc/examples/cat-signed-doc.rs b/rust/signed_doc/examples/cat-signed-doc.rs index 381f72852d6..20fe9c2b1dd 100644 --- a/rust/signed_doc/examples/cat-signed-doc.rs +++ b/rust/signed_doc/examples/cat-signed-doc.rs @@ -11,8 +11,8 @@ use std::{ path::PathBuf, }; +use catalyst_signed_doc::CatalystSignedDocument; use clap::Parser; -use signed_doc::CatalystSignedDocument; /// Hermes cli commands #[derive(clap::Parser)] @@ -42,7 +42,7 @@ impl Cli { Self::InspectBytes { cose_sign_str } => hex::decode(&cose_sign_str)?, }; println!("Bytes read:\n{}\n", hex::encode(&cose_bytes)); - let cat_signed_doc: CatalystSignedDocument = cose_bytes.try_into()?; + let cat_signed_doc: CatalystSignedDocument = cose_bytes.as_slice().try_into()?; println!("{cat_signed_doc}"); Ok(()) } diff --git a/rust/signed_doc/examples/mk_signed_doc.rs b/rust/signed_doc/examples/mk_signed_doc.rs index 88fea415ad8..1074d55e949 100644 --- a/rust/signed_doc/examples/mk_signed_doc.rs +++ b/rust/signed_doc/examples/mk_signed_doc.rs @@ -8,13 +8,13 @@ use std::{ path::PathBuf, }; +use catalyst_signed_doc::{DocumentRef, KidUri, Metadata, UuidV7}; use clap::Parser; use coset::{iana::CoapContentFormat, CborSerializable}; use ed25519_dalek::{ ed25519::signature::Signer, pkcs8::{DecodePrivateKey, DecodePublicKey}, }; -use signed_doc::{DocumentRef, KidUri, Metadata, UuidV7}; fn main() { if let Err(err) = Cli::parse().exec() { diff --git a/rust/signed_doc/src/error.rs b/rust/signed_doc/src/error.rs new file mode 100644 index 00000000000..a33e4176b4c --- /dev/null +++ b/rust/signed_doc/src/error.rs @@ -0,0 +1,19 @@ +//! Catalyst Signed Document errors. + +/// Catalyst Signed Document error. +#[derive(thiserror::Error, Debug)] +#[error("Catalyst Signed Document Error: {0:#?}")] +pub struct Error(pub(crate) Vec); + +impl From> for Error { + fn from(e: Vec) -> Self { + Error(e) + } +} + +impl Error { + /// List of errors. + pub fn errors(&self) -> &Vec { + &self.0 + } +} diff --git a/rust/signed_doc/src/lib.rs b/rust/signed_doc/src/lib.rs index 8c3d3d2afb6..5f1c17b0865 100644 --- a/rust/signed_doc/src/lib.rs +++ b/rust/signed_doc/src/lib.rs @@ -6,8 +6,9 @@ use std::{ }; use anyhow::anyhow; -use coset::CborSerializable; +use coset::{CborSerializable, CoseSignature}; +mod error; mod metadata; mod payload; mod signature; @@ -15,6 +16,7 @@ mod signature; pub use metadata::{DocumentRef, Metadata, UuidV7}; use payload::JsonContent; pub use signature::KidUri; +use signature::Signatures; /// Inner type that holds the Catalyst Signed Document with parsing errors. #[derive(Default)] @@ -23,6 +25,8 @@ struct InnerCatalystSignedDocument { metadata: Metadata, /// Document Payload viewed as JSON Content payload: JsonContent, + /// Signatures + signatures: Signatures, /// Raw COSE Sign data cose_sign: coset::CoseSign, } @@ -54,7 +58,7 @@ impl Display for CatalystSignedDocument { } impl TryFrom<&[u8]> for CatalystSignedDocument { - type Error = Vec; + type Error = error::Error; fn try_from(cose_bytes: &[u8]) -> Result { // Try reading as a tagged COSE SIGN, otherwise try reading as untagged. @@ -67,7 +71,7 @@ impl TryFrom<&[u8]> for CatalystSignedDocument { let mut payload = JsonContent::default(); if let Some(bytes) = &cose_sign.payload { - match JsonContent::try_from((bytes, metadata.content_encoding())) { + match JsonContent::try_from((bytes.as_ref(), metadata.content_encoding())) { Ok(c) => payload = c, Err(e) => { content_errors.push(anyhow!("Invalid Payload: {e}")); @@ -77,11 +81,27 @@ impl TryFrom<&[u8]> for CatalystSignedDocument { content_errors.push(anyhow!("COSE payload is empty")); }; + let mut signatures = Signatures::default(); + match Signatures::try_from(&cose_sign.signatures) { + Ok(s) => signatures = s, + Err(errors) => { + for e in errors.errors() { + content_errors.push(anyhow!("{e}")); + } + }, + } + let inner = InnerCatalystSignedDocument { metadata, payload, + signatures, cose_sign, }; + + if !content_errors.is_empty() { + return Err(error::Error(content_errors)); + } + Ok(CatalystSignedDocument { inner: Arc::new(inner), }) @@ -132,4 +152,22 @@ impl CatalystSignedDocument { pub fn doc_section(&self) -> Option { self.inner.metadata.doc_section() } + + /// Return Raw COSE SIGN bytes. + #[must_use] + pub fn cose_sign_bytes(&self) -> Vec { + self.inner.cose_sign.clone().to_vec().unwrap_or_default() + } + + /// Return a list of signature KIDs. + #[must_use] + pub fn signature_kids(&self) -> Vec { + self.inner.signatures.kids() + } + + /// Return a list of signatures. + #[must_use] + pub fn signatures(&self) -> Vec { + self.inner.signatures.signatures() + } } diff --git a/rust/signed_doc/src/metadata/mod.rs b/rust/signed_doc/src/metadata/mod.rs index 433e6ddb97e..67bc5d02ddd 100644 --- a/rust/signed_doc/src/metadata/mod.rs +++ b/rust/signed_doc/src/metadata/mod.rs @@ -49,10 +49,10 @@ pub struct Metadata { } impl Metadata { - /// Are there any validation errors (as opposed to structural errors). + /// Returns true if metadata has no validation errors. #[must_use] pub fn is_valid(&self) -> bool { - !self.content_errors.is_empty() + self.content_errors.is_empty() } /// Return Document Type `UUIDv4`. @@ -144,7 +144,7 @@ impl Default for Metadata { } impl TryFrom<&coset::ProtectedHeader> for Metadata { - type Error = Vec; + type Error = crate::error::Error; #[allow(clippy::too_many_lines)] fn try_from(protected: &coset::ProtectedHeader) -> Result { @@ -254,7 +254,7 @@ impl TryFrom<&coset::ProtectedHeader> for Metadata { if errors.is_empty() { Ok(metadata) } else { - Err(errors) + Err(errors.into()) } } } diff --git a/rust/signed_doc/src/payload/json.rs b/rust/signed_doc/src/payload/json.rs index 51be6193516..e5dc349673f 100644 --- a/rust/signed_doc/src/payload/json.rs +++ b/rust/signed_doc/src/payload/json.rs @@ -20,22 +20,19 @@ impl TryFrom<&[u8]> for Content { } } -impl TryFrom<(&Vec, Option)> for Content { +impl TryFrom<(&[u8], Option)> for Content { type Error = anyhow::Error; - fn try_from( - (value, encoding): (&Vec, Option), - ) -> Result { + fn try_from((value, encoding): (&[u8], Option)) -> Result { if let Some(content_encoding) = encoding { - match content_encoding.decode(value) { - Ok(decompressed) => { - return Self::try_from(decompressed.as_slice()); - }, + match content_encoding.decode(&value.to_vec()) { + Ok(decompressed) => Self::try_from(decompressed.as_slice()), Err(e) => { - anyhow::bail!("Failed to decode {encoding:?}: {e}"); + anyhow::bail!("Failed to decode {encoding:?} content: {e}"); }, } + } else { + Self::try_from(value) } - Self::try_from(value.as_ref()) } } diff --git a/rust/signed_doc/src/signature/mod.rs b/rust/signed_doc/src/signature/mod.rs index 9ce4d711914..b60d18e30d2 100644 --- a/rust/signed_doc/src/signature/mod.rs +++ b/rust/signed_doc/src/signature/mod.rs @@ -1,2 +1,57 @@ //! Catalyst Signed Document COSE Signature information. + pub use catalyst_types::kid_uri::KidUri; +use coset::CoseSignature; + +/// Catalyst Signed Document COSE Signature. +#[derive(Debug)] +pub struct Signature { + /// Key ID + kid: KidUri, + /// COSE Signature + signature: CoseSignature, +} + +/// List of Signatures. +#[derive(Default)] +pub struct Signatures(Vec); + +impl Signatures { + /// List of signature Key IDs. + pub fn kids(&self) -> Vec { + self.0.iter().map(|sig| sig.kid.clone()).collect() + } + + /// List of signatures. + pub fn signatures(&self) -> Vec { + self.0.iter().map(|sig| sig.signature.clone()).collect() + } +} + +impl TryFrom<&Vec> for Signatures { + type Error = crate::error::Error; + + fn try_from(value: &Vec) -> Result { + let mut signatures = Vec::new(); + let mut errors = Vec::new(); + value + .iter() + .cloned() + .enumerate() + .for_each(|(idx, signature)| { + match KidUri::try_from(signature.protected.header.key_id.as_ref()) { + Ok(kid) => signatures.push(Signature { kid, signature }), + Err(e) => { + errors.push(anyhow::anyhow!( + "Signature at index {idx} has valid Catalyst Key Id: {e}" + )); + }, + } + }); + if errors.is_empty() { + Err(errors.into()) + } else { + Ok(Signatures(signatures)) + } + } +} From 4455ed2c8e94bd01f15e6ad30aeffa2e2fb74289 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Fri, 10 Jan 2025 14:21:17 +0200 Subject: [PATCH 47/71] update document content --- rust/signed_doc/src/content.rs | 50 ++++++++ rust/signed_doc/src/lib.rs | 112 +++++++----------- .../src/metadata/content_encoding.rs | 5 +- rust/signed_doc/src/metadata/content_type.rs | 2 +- rust/signed_doc/src/metadata/mod.rs | 40 ------- rust/signed_doc/src/payload/json.rs | 38 ------ rust/signed_doc/src/payload/mod.rs | 32 ----- 7 files changed, 94 insertions(+), 185 deletions(-) create mode 100644 rust/signed_doc/src/content.rs delete mode 100644 rust/signed_doc/src/payload/json.rs delete mode 100644 rust/signed_doc/src/payload/mod.rs diff --git a/rust/signed_doc/src/content.rs b/rust/signed_doc/src/content.rs new file mode 100644 index 00000000000..3e1fc415631 --- /dev/null +++ b/rust/signed_doc/src/content.rs @@ -0,0 +1,50 @@ +//! Catalyst Signed Document Content Payload + +use crate::metadata::{ContentEncoding, ContentType}; + +/// Decompressed Document Content type bytes. +#[derive(Debug, Clone, PartialEq)] +pub struct Content(Vec, ContentType); + +impl Content { + /// Creates a new `Content` value, + /// verifies a Document's content, that it is correctly encoded and it corresponds and + /// parsed to the specified type + pub fn new( + mut content: Vec, content_type: ContentType, encoding: Option, + ) -> anyhow::Result { + if let Some(content_encoding) = encoding { + content = content_encoding + .decode(content.as_slice()) + .map_err(|e| anyhow::anyhow!("Failed to decode {encoding:?} content: {e}"))?; + } + + match content_type { + ContentType::Json => { + serde_json::from_slice::(content.as_slice())?; + }, + ContentType::Cbor => { + // TODO impelement a CBOR parsing validation + }, + } + + Ok(Self(content, content_type)) + } + + /// Return `true` if Document's content type is Json + #[must_use] + pub fn is_json(&self) -> bool { + matches!(self.1, ContentType::Json) + } + + /// Return `true` if Document's content type is Json + #[must_use] + pub fn is_cbor(&self) -> bool { + matches!(self.1, ContentType::Cbor) + } + + /// Return content bytes + pub fn bytes(&self) -> Vec { + self.0.clone() + } +} diff --git a/rust/signed_doc/src/lib.rs b/rust/signed_doc/src/lib.rs index 5f1c17b0865..9a1cbbb8da7 100644 --- a/rust/signed_doc/src/lib.rs +++ b/rust/signed_doc/src/lib.rs @@ -6,29 +6,26 @@ use std::{ }; use anyhow::anyhow; +use content::Content; use coset::{CborSerializable, CoseSignature}; +mod content; mod error; mod metadata; -mod payload; mod signature; pub use metadata::{DocumentRef, Metadata, UuidV7}; -use payload::JsonContent; pub use signature::KidUri; use signature::Signatures; /// Inner type that holds the Catalyst Signed Document with parsing errors. -#[derive(Default)] struct InnerCatalystSignedDocument { /// Document Metadata metadata: Metadata, - /// Document Payload viewed as JSON Content - payload: JsonContent, + /// Document Content + content: Content, /// Signatures signatures: Signatures, - /// Raw COSE Sign data - cose_sign: coset::CoseSign, } /// Keep all the contents private. @@ -43,15 +40,9 @@ pub struct CatalystSignedDocument { impl Display for CatalystSignedDocument { fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { writeln!(f, "{}", self.inner.metadata)?; - writeln!(f, "{:#?}\n", self.inner.payload)?; writeln!(f, "Signature Information [")?; - for signature in &self.inner.cose_sign.signatures { - writeln!( - f, - " {} 0x{:#}", - String::from_utf8_lossy(&signature.protected.header.key_id), - hex::encode(signature.signature.as_slice()) - )?; + for kid in &self.inner.signatures.kids() { + writeln!(f, " {kid}")?; } writeln!(f, "]\n") } @@ -67,44 +58,47 @@ impl TryFrom<&[u8]> for CatalystSignedDocument { let metadata = Metadata::try_from(&cose_sign.protected)?; - let mut content_errors = Vec::new(); - let mut payload = JsonContent::default(); - - if let Some(bytes) = &cose_sign.payload { - match JsonContent::try_from((bytes.as_ref(), metadata.content_encoding())) { - Ok(c) => payload = c, - Err(e) => { - content_errors.push(anyhow!("Invalid Payload: {e}")); - }, - } - } else { - content_errors.push(anyhow!("COSE payload is empty")); - }; + let mut errors = Vec::new(); let mut signatures = Signatures::default(); match Signatures::try_from(&cose_sign.signatures) { Ok(s) => signatures = s, - Err(errors) => { - for e in errors.errors() { - content_errors.push(anyhow!("{e}")); + Err(sign_errors) => { + for e in sign_errors.errors() { + errors.push(anyhow!("{e}")); } }, } - let inner = InnerCatalystSignedDocument { - metadata, - payload, - signatures, - cose_sign, - }; - - if !content_errors.is_empty() { - return Err(error::Error(content_errors)); + if let Some(payload) = cose_sign.payload { + match Content::new( + payload, + metadata.content_type(), + metadata.content_encoding(), + ) { + Ok(content) => { + if !errors.is_empty() { + return Err(error::Error(errors)); + } + + Ok(CatalystSignedDocument { + inner: InnerCatalystSignedDocument { + metadata, + content, + signatures, + } + .into(), + }) + }, + Err(e) => { + errors.push(anyhow::anyhow!("Invalid Document Content: {e}")); + Err(error::Error(errors)) + }, + } + } else { + errors.push(anyhow!("Document content is missing")); + Err(error::Error(errors)) } - - Ok(CatalystSignedDocument { - inner: Arc::new(inner), - }) } } @@ -129,34 +123,10 @@ impl CatalystSignedDocument { self.inner.metadata.doc_ver() } - /// Return Last Document Reference `Option`. - #[must_use] - pub fn doc_ref(&self) -> Option { - self.inner.metadata.doc_ref() - } - - /// Return Document Template `Option`. - #[must_use] - pub fn doc_template(&self) -> Option { - self.inner.metadata.doc_template() - } - - /// Return Document Reply `Option`. - #[must_use] - pub fn doc_reply(&self) -> Option { - self.inner.metadata.doc_reply() - } - - /// Return Document Reply `Option`. - #[must_use] - pub fn doc_section(&self) -> Option { - self.inner.metadata.doc_section() - } - - /// Return Raw COSE SIGN bytes. + /// Return document `Content`. #[must_use] - pub fn cose_sign_bytes(&self) -> Vec { - self.inner.cose_sign.clone().to_vec().unwrap_or_default() + pub fn document_content(&self) -> &Content { + &self.inner.content } /// Return a list of signature KIDs. diff --git a/rust/signed_doc/src/metadata/content_encoding.rs b/rust/signed_doc/src/metadata/content_encoding.rs index 1f4fd0444c2..3134b9ef7bd 100644 --- a/rust/signed_doc/src/metadata/content_encoding.rs +++ b/rust/signed_doc/src/metadata/content_encoding.rs @@ -56,12 +56,11 @@ impl TryFrom<&coset::cbor::Value> for ContentEncoding { impl ContentEncoding { /// Decompress a Brotli payload - pub fn decode(self, payload: &Vec) -> anyhow::Result> { + pub fn decode(self, mut payload: &[u8]) -> anyhow::Result> { match self { Self::Brotli => { let mut buf = Vec::new(); - let mut bytes = payload.as_slice(); - brotli::BrotliDecompress(&mut bytes, &mut buf)?; + brotli::BrotliDecompress(&mut payload, &mut buf)?; Ok(buf) }, } diff --git a/rust/signed_doc/src/metadata/content_type.rs b/rust/signed_doc/src/metadata/content_type.rs index c2ce21f0604..4d1ed2eb718 100644 --- a/rust/signed_doc/src/metadata/content_type.rs +++ b/rust/signed_doc/src/metadata/content_type.rs @@ -9,7 +9,7 @@ use coset::iana::CoapContentFormat; use serde::{de, Deserialize, Deserializer}; /// Payload Content Type. -#[derive(Copy, Clone, Debug)] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum ContentType { /// 'application/cbor' Cbor, diff --git a/rust/signed_doc/src/metadata/mod.rs b/rust/signed_doc/src/metadata/mod.rs index 67bc5d02ddd..e6c30faa400 100644 --- a/rust/signed_doc/src/metadata/mod.rs +++ b/rust/signed_doc/src/metadata/mod.rs @@ -43,18 +43,9 @@ pub struct Metadata { /// Additional Metadata Fields. #[serde(flatten)] extra: AdditionalFields, - /// Metadata Content Errors - #[serde(skip)] - content_errors: Vec, } impl Metadata { - /// Returns true if metadata has no validation errors. - #[must_use] - pub fn is_valid(&self) -> bool { - self.content_errors.is_empty() - } - /// Return Document Type `UUIDv4`. #[must_use] pub fn doc_type(&self) -> uuid::Uuid { @@ -84,36 +75,6 @@ impl Metadata { pub fn content_encoding(&self) -> Option { self.content_encoding } - - /// Return Last Document Reference `Option`. - #[must_use] - pub fn doc_ref(&self) -> Option { - self.extra.doc_ref - } - - /// Return Document Template `Option`. - #[must_use] - pub fn doc_template(&self) -> Option { - self.extra.template - } - - /// Return Document Reply `Option`. - #[must_use] - pub fn doc_reply(&self) -> Option { - self.extra.reply - } - - /// Return Document Section `Option`. - #[must_use] - pub fn doc_section(&self) -> Option { - self.extra.section.clone() - } - - /// List of Content Errors. - #[must_use] - pub fn content_errors(&self) -> &Vec { - &self.content_errors - } } impl Display for Metadata { @@ -138,7 +99,6 @@ impl Default for Metadata { content_type: ContentType::default(), content_encoding: None, extra: AdditionalFields::default(), - content_errors: Vec::new(), } } } diff --git a/rust/signed_doc/src/payload/json.rs b/rust/signed_doc/src/payload/json.rs deleted file mode 100644 index e5dc349673f..00000000000 --- a/rust/signed_doc/src/payload/json.rs +++ /dev/null @@ -1,38 +0,0 @@ -//! JSON Content -use super::Content; -use crate::metadata::ContentEncoding; - -/// JSON encoded content -pub type Json = Content; - -impl Default for Json { - fn default() -> Self { - serde_json::Value::Object(serde_json::Map::new()).into() - } -} - -impl TryFrom<&[u8]> for Content { - type Error = anyhow::Error; - - fn try_from(value: &[u8]) -> Result { - serde_json::from_slice(value) - .map_err(|e| anyhow::anyhow!("Failed to parse any JSON content: {e}")) - } -} - -impl TryFrom<(&[u8], Option)> for Content { - type Error = anyhow::Error; - - fn try_from((value, encoding): (&[u8], Option)) -> Result { - if let Some(content_encoding) = encoding { - match content_encoding.decode(&value.to_vec()) { - Ok(decompressed) => Self::try_from(decompressed.as_slice()), - Err(e) => { - anyhow::bail!("Failed to decode {encoding:?} content: {e}"); - }, - } - } else { - Self::try_from(value) - } - } -} diff --git a/rust/signed_doc/src/payload/mod.rs b/rust/signed_doc/src/payload/mod.rs deleted file mode 100644 index 06d43aac31b..00000000000 --- a/rust/signed_doc/src/payload/mod.rs +++ /dev/null @@ -1,32 +0,0 @@ -//! Catalyst Signed Document JSON Payload - -use std::fmt::{Display, Formatter}; - -use serde::{Deserialize, Deserializer}; - -mod json; - -pub use json::Json as JsonContent; - -/// JSON Content -#[derive(Debug)] -pub struct Content(T); - -impl From for Content { - fn from(value: T) -> Self { - Self(value) - } -} - -impl Display for Content { - fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { - write!(f, "{}", self.0) - } -} - -impl<'de, T: serde::Deserialize<'de>> Deserialize<'de> for Content { - fn deserialize(deserializer: D) -> Result - where D: Deserializer<'de> { - T::deserialize(deserializer).map(std::convert::Into::into) - } -} From f8f42353fa3f647c51a83376a67faf2f106a0f7a Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Fri, 10 Jan 2025 15:13:52 +0200 Subject: [PATCH 48/71] remove parsing check --- rust/signed_doc/src/content.rs | 13 ++----------- rust/signed_doc/src/lib.rs | 2 +- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/rust/signed_doc/src/content.rs b/rust/signed_doc/src/content.rs index 3e1fc415631..3cc76d23af1 100644 --- a/rust/signed_doc/src/content.rs +++ b/rust/signed_doc/src/content.rs @@ -19,15 +19,6 @@ impl Content { .map_err(|e| anyhow::anyhow!("Failed to decode {encoding:?} content: {e}"))?; } - match content_type { - ContentType::Json => { - serde_json::from_slice::(content.as_slice())?; - }, - ContentType::Cbor => { - // TODO impelement a CBOR parsing validation - }, - } - Ok(Self(content, content_type)) } @@ -44,7 +35,7 @@ impl Content { } /// Return content bytes - pub fn bytes(&self) -> Vec { - self.0.clone() + pub fn bytes(&self) -> &[u8] { + self.0.as_slice() } } diff --git a/rust/signed_doc/src/lib.rs b/rust/signed_doc/src/lib.rs index 9a1cbbb8da7..b1740c16495 100644 --- a/rust/signed_doc/src/lib.rs +++ b/rust/signed_doc/src/lib.rs @@ -125,7 +125,7 @@ impl CatalystSignedDocument { /// Return document `Content`. #[must_use] - pub fn document_content(&self) -> &Content { + pub fn doc_content(&self) -> &Content { &self.inner.content } From 40acd235cfd11ba121d13d76448ec30a3a8a410d Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Fri, 10 Jan 2025 16:13:27 +0200 Subject: [PATCH 49/71] wip --- rust/signed_doc/src/lib.rs | 94 ++++++++++++++-------------- rust/signed_doc/src/signature/mod.rs | 2 +- 2 files changed, 49 insertions(+), 47 deletions(-) diff --git a/rust/signed_doc/src/lib.rs b/rust/signed_doc/src/lib.rs index b1740c16495..8ebdf5aa349 100644 --- a/rust/signed_doc/src/lib.rs +++ b/rust/signed_doc/src/lib.rs @@ -7,7 +7,7 @@ use std::{ use anyhow::anyhow; use content::Content; -use coset::{CborSerializable, CoseSignature}; +use coset::CborSerializable; mod content; mod error; @@ -24,6 +24,8 @@ struct InnerCatalystSignedDocument { metadata: Metadata, /// Document Content content: Content, + /// Document Author + author: KidUri, /// Signatures signatures: Signatures, } @@ -56,48 +58,54 @@ impl TryFrom<&[u8]> for CatalystSignedDocument { let cose_sign = coset::CoseSign::from_slice(cose_bytes) .map_err(|e| vec![anyhow::anyhow!("Invalid COSE Sign document: {e}")])?; - let metadata = Metadata::try_from(&cose_sign.protected)?; - let mut errors = Vec::new(); - let mut signatures = Signatures::default(); - match Signatures::try_from(&cose_sign.signatures) { - Ok(s) => signatures = s, - Err(sign_errors) => { - for e in sign_errors.errors() { - errors.push(anyhow!("{e}")); - } + let metadata = Metadata::try_from(&cose_sign.protected).map_or_else( + |e| { + errors.extend(e.0); + None + }, + Some, + ); + let signatures = Signatures::try_from(&cose_sign.signatures).map_or_else( + |e| { + errors.extend(e.0); + None }, + Some, + ); + let author = signatures.as_ref().and_then(|s| s.kids().first().cloned()); + + if cose_sign.payload.is_none() { + errors.push(anyhow!("Document Content is missing")); + } + if author.is_none() { + errors.push(anyhow!("Document Author is missing")); } - if let Some(payload) = cose_sign.payload { - match Content::new( - payload, - metadata.content_type(), - metadata.content_encoding(), - ) { - Ok(content) => { - if !errors.is_empty() { - return Err(error::Error(errors)); + match (cose_sign.payload, author, metadata, signatures) { + (Some(payload), Some(author), Some(metadata), Some(signatures)) => { + let content = Content::new( + payload, + metadata.content_type(), + metadata.content_encoding(), + ) + .map_err(|e| { + errors.push(anyhow!("Invalid Document Content: {e}")); + errors + })?; + + Ok(CatalystSignedDocument { + inner: InnerCatalystSignedDocument { + metadata, + content, + author, + signatures, } - - Ok(CatalystSignedDocument { - inner: InnerCatalystSignedDocument { - metadata, - content, - signatures, - } - .into(), - }) - }, - Err(e) => { - errors.push(anyhow::anyhow!("Invalid Document Content: {e}")); - Err(error::Error(errors)) - }, - } - } else { - errors.push(anyhow!("Document content is missing")); - Err(error::Error(errors)) + .into(), + }) + }, + _ => Err(error::Error(errors)), } } } @@ -129,15 +137,9 @@ impl CatalystSignedDocument { &self.inner.content } - /// Return a list of signature KIDs. - #[must_use] - pub fn signature_kids(&self) -> Vec { - self.inner.signatures.kids() - } - - /// Return a list of signatures. + /// Return a Document's author #[must_use] - pub fn signatures(&self) -> Vec { - self.inner.signatures.signatures() + pub fn author(&self) -> KidUri { + self.inner.author.clone() } } diff --git a/rust/signed_doc/src/signature/mod.rs b/rust/signed_doc/src/signature/mod.rs index b60d18e30d2..a53ce622e27 100644 --- a/rust/signed_doc/src/signature/mod.rs +++ b/rust/signed_doc/src/signature/mod.rs @@ -13,7 +13,6 @@ pub struct Signature { } /// List of Signatures. -#[derive(Default)] pub struct Signatures(Vec); impl Signatures { @@ -23,6 +22,7 @@ impl Signatures { } /// List of signatures. + #[allow(dead_code)] pub fn signatures(&self) -> Vec { self.0.iter().map(|sig| sig.signature.clone()).collect() } From aca802a4df935bd24f2107ff7ebdf0b5d871faa6 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Fri, 10 Jan 2025 16:34:01 +0200 Subject: [PATCH 50/71] refactor Metadata decoding --- rust/signed_doc/src/metadata/content_type.rs | 6 - rust/signed_doc/src/metadata/document_id.rs | 5 - rust/signed_doc/src/metadata/document_type.rs | 5 - .../src/metadata/document_version.rs | 5 - rust/signed_doc/src/metadata/mod.rs | 164 ++++++++---------- 5 files changed, 73 insertions(+), 112 deletions(-) diff --git a/rust/signed_doc/src/metadata/content_type.rs b/rust/signed_doc/src/metadata/content_type.rs index 4d1ed2eb718..b45105a04e6 100644 --- a/rust/signed_doc/src/metadata/content_type.rs +++ b/rust/signed_doc/src/metadata/content_type.rs @@ -17,12 +17,6 @@ pub enum ContentType { Json, } -impl Default for ContentType { - fn default() -> Self { - Self::Json - } -} - impl Display for ContentType { fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { match self { diff --git a/rust/signed_doc/src/metadata/document_id.rs b/rust/signed_doc/src/metadata/document_id.rs index 258225b94ca..ca71ff260b9 100644 --- a/rust/signed_doc/src/metadata/document_id.rs +++ b/rust/signed_doc/src/metadata/document_id.rs @@ -9,11 +9,6 @@ use super::UuidV7; pub struct DocumentId(UuidV7); impl DocumentId { - /// Generates a zeroed out `UUIDv7` that can never be valid. - pub fn invalid() -> Self { - Self(UuidV7::invalid()) - } - /// Returns the `uuid::Uuid` type. #[must_use] pub fn uuid(&self) -> uuid::Uuid { diff --git a/rust/signed_doc/src/metadata/document_type.rs b/rust/signed_doc/src/metadata/document_type.rs index 27e8e3e0459..f1383d222e5 100644 --- a/rust/signed_doc/src/metadata/document_type.rs +++ b/rust/signed_doc/src/metadata/document_type.rs @@ -9,11 +9,6 @@ use super::UuidV4; pub struct DocumentType(UuidV4); impl DocumentType { - /// Generates a zeroed out `UUIDv4` that can never be valid. - pub fn invalid() -> Self { - Self(UuidV4::invalid()) - } - /// Returns the `uuid::Uuid` type. #[must_use] pub fn uuid(&self) -> uuid::Uuid { diff --git a/rust/signed_doc/src/metadata/document_version.rs b/rust/signed_doc/src/metadata/document_version.rs index ed483d145c4..b390e147eed 100644 --- a/rust/signed_doc/src/metadata/document_version.rs +++ b/rust/signed_doc/src/metadata/document_version.rs @@ -8,11 +8,6 @@ use super::UuidV7; pub struct DocumentVersion(UuidV7); impl DocumentVersion { - /// Generates a zeroed out `UUIDv7` that can never be valid. - pub fn invalid() -> Self { - Self(UuidV7::invalid()) - } - /// Returns the `uuid::Uuid` type. #[must_use] pub fn uuid(&self) -> uuid::Uuid { diff --git a/rust/signed_doc/src/metadata/mod.rs b/rust/signed_doc/src/metadata/mod.rs index e6c30faa400..143b1a7f97c 100644 --- a/rust/signed_doc/src/metadata/mod.rs +++ b/rust/signed_doc/src/metadata/mod.rs @@ -35,10 +35,10 @@ pub struct Metadata { /// Document Version `UUIDv7`. ver: DocumentVersion, /// Document Payload Content Type. - #[serde(default, rename = "content-type")] + #[serde(rename = "content-type")] content_type: ContentType, /// Document Payload Content Encoding. - #[serde(default, rename = "content-encoding")] + #[serde(rename = "content-encoding")] content_encoding: Option, /// Additional Metadata Fields. #[serde(flatten)] @@ -90,71 +90,46 @@ impl Display for Metadata { } } -impl Default for Metadata { - fn default() -> Self { - Self { - doc_type: DocumentType::invalid(), - id: DocumentId::invalid(), - ver: DocumentVersion::invalid(), - content_type: ContentType::default(), - content_encoding: None, - extra: AdditionalFields::default(), - } - } -} - impl TryFrom<&coset::ProtectedHeader> for Metadata { type Error = crate::error::Error; - #[allow(clippy::too_many_lines)] fn try_from(protected: &coset::ProtectedHeader) -> Result { - let mut metadata = Metadata::default(); let mut errors = Vec::new(); - match protected.header.content_type.as_ref() { - Some(iana_content_type) => { - match ContentType::try_from(iana_content_type) { - Ok(content_type) => metadata.content_type = content_type, - Err(e) => { - errors.push(anyhow!("Invalid Document Content-Type: {e}")); - }, - } - }, - None => { - errors.push(anyhow!( - "COSE document protected header `content-type` field is missing" - )); - }, + let mut content_type = None; + if let Some(value) = protected.header.content_type.as_ref() { + match ContentType::try_from(value) { + Ok(ct) => content_type = Some(ct), + Err(e) => errors.push(anyhow!("Invalid Document Content-Type: {e}")), + } + } else { + errors.push(anyhow!( + "Invalid COSE protected header, missing Content-Type field" + )); } + let mut content_encoding = None; if let Some(value) = cose_protected_header_find( protected, |key| matches!(key, coset::Label::Text(label) if label.eq_ignore_ascii_case(CONTENT_ENCODING_KEY)), ) { match ContentEncoding::try_from(value) { - Ok(encoding) => { - metadata.content_encoding = Some(encoding); - }, - Err(e) => { - errors.push(anyhow!("Invalid Document Content Encoding: {e}")); - }, + Ok(ce) => content_encoding = Some(ce), + Err(e) => errors.push(anyhow!("Invalid Document Content Encoding: {e}")), } } else { errors.push(anyhow!( - "Invalid COSE document protected header '{CONTENT_ENCODING_KEY}' is missing" + "Invalid COSE protected header, missing Content-Encoding field" )); } - if let Some(doc_type) = cose_protected_header_find(protected, |key| { + let mut doc_type = None; + if let Some(value) = cose_protected_header_find(protected, |key| { key == &coset::Label::Text("type".to_string()) }) { - match UuidV4::try_from(doc_type) { - Ok(doc_type_uuid) => { - metadata.doc_type = doc_type_uuid.into(); - }, - Err(e) => { - errors.push(anyhow!("Document `type` is invalid: {e}")); - }, + match UuidV4::try_from(value) { + Ok(uuid) => doc_type = Some(uuid), + Err(e) => errors.push(anyhow!("Document `type` is invalid: {e}")), } } else { errors.push(anyhow!( @@ -162,59 +137,66 @@ impl TryFrom<&coset::ProtectedHeader> for Metadata { )); } - match cose_protected_header_find(protected, |key| { + let mut id = None; + if let Some(value) = cose_protected_header_find(protected, |key| { key == &coset::Label::Text("id".to_string()) }) { - Some(doc_id) => { - match UuidV7::try_from(doc_id) { - Ok(doc_id_uuid) => { - metadata.id = doc_id_uuid.into(); - }, - Err(e) => { - errors.push(anyhow!("Document `id` is invalid: {e}")); - }, - } - }, - None => errors.push(anyhow!("Invalid COSE protected header, missing `id` field")), - }; + match UuidV7::try_from(value) { + Ok(uuid) => id = Some(uuid), + Err(e) => errors.push(anyhow!("Document `id` is invalid: {e}")), + } + } else { + errors.push(anyhow!("Invalid COSE protected header, missing `id` field")); + } - match cose_protected_header_find(protected, |key| { + let mut ver = None; + if let Some(value) = cose_protected_header_find(protected, |key| { key == &coset::Label::Text("ver".to_string()) }) { - Some(doc_ver) => { - match UuidV7::try_from(doc_ver) { - Ok(doc_ver_uuid) => { - if doc_ver_uuid.uuid() < metadata.id.uuid() { - errors.push(anyhow!( - "Document Version {doc_ver_uuid} cannot be smaller than Document ID {}", metadata.id - )); - } else { - metadata.ver = doc_ver_uuid.into(); - } - }, - Err(e) => { - errors.push(anyhow!( - "Invalid COSE protected header `ver` field, err: {e}" - )); - }, - } - }, - None => { - errors.push(anyhow!( - "Invalid COSE protected header, missing `ver` field" - )); - }, + match UuidV7::try_from(value) { + Ok(uuid) => ver = Some(uuid), + Err(e) => errors.push(anyhow!("Document `ver` is invalid: {e}")), + } + } else { + errors.push(anyhow!( + "Invalid COSE protected header, missing `ver` field" + )); } - match AdditionalFields::try_from(protected) { - Ok(extra) => metadata.extra = extra, - Err(e) => errors.extend(e), - }; + let extra = AdditionalFields::try_from(protected).map_or_else( + |e| { + errors.extend(e); + None + }, + Some, + ); + + match (content_type, content_encoding, id, doc_type, ver, extra) { + ( + Some(content_type), + content_encoding, + Some(id), + Some(doc_type), + Some(ver), + Some(extra), + ) => { + if ver < id { + errors.push(anyhow!( + "Document Version {ver} cannot be smaller than Document ID {id}", + )); + return Err(crate::error::Error(errors)); + } - if errors.is_empty() { - Ok(metadata) - } else { - Err(errors.into()) + Ok(Self { + doc_type: doc_type.into(), + id: id.into(), + ver: ver.into(), + content_encoding, + content_type, + extra, + }) + }, + _ => Err(crate::error::Error(errors)), } } } From b713b8e142d18b34dde79a2be240b64ed8413476 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Rosales?= Date: Fri, 10 Jan 2025 22:13:31 -0600 Subject: [PATCH 51/71] fix(rust/signed_doc): add serde::Serialize to AdditionalFields and DocumentRef --- rust/signed_doc/Cargo.toml | 2 +- rust/signed_doc/src/metadata/additional_fields.rs | 2 +- rust/signed_doc/src/metadata/document_ref.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/rust/signed_doc/Cargo.toml b/rust/signed_doc/Cargo.toml index 38c77d36b2d..5a4d970346d 100644 --- a/rust/signed_doc/Cargo.toml +++ b/rust/signed_doc/Cargo.toml @@ -12,7 +12,7 @@ workspace = true [dependencies] cardano-blockchain-types = { version = "0.0.1", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "v0.0.11" } -catalyst-types = { version = "0.0.1", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250107-00" } +catalyst-types = { version = "0.0.1", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250111-00" } anyhow = "1.0.95" serde = { version = "1.0.217", features = ["derive"] } serde_json = "1.0.134" diff --git a/rust/signed_doc/src/metadata/additional_fields.rs b/rust/signed_doc/src/metadata/additional_fields.rs index c09b36e3142..5612f1f1291 100644 --- a/rust/signed_doc/src/metadata/additional_fields.rs +++ b/rust/signed_doc/src/metadata/additional_fields.rs @@ -7,7 +7,7 @@ use super::{cose_protected_header_find, DocumentRef}; /// Additional Metadata Fields. /// /// These values are extracted from the COSE Sign protected header labels. -#[derive(Default, Debug, serde::Deserialize)] +#[derive(Default, Debug, serde::Serialize, serde::Deserialize)] pub(super) struct AdditionalFields { /// Reference to the latest document. #[serde(rename = "ref")] diff --git a/rust/signed_doc/src/metadata/document_ref.rs b/rust/signed_doc/src/metadata/document_ref.rs index d5d5c95a3ed..ed540d4430f 100644 --- a/rust/signed_doc/src/metadata/document_ref.rs +++ b/rust/signed_doc/src/metadata/document_ref.rs @@ -2,7 +2,7 @@ use super::UuidV7; /// Reference to a Document. -#[derive(Copy, Clone, Debug, serde::Deserialize)] +#[derive(Copy, Clone, Debug, serde::Serialize, serde::Deserialize)] #[serde(untagged)] pub enum DocumentRef { /// Reference to the latest document From 8c237863d83662132e6f546f3e0643e77c6c0fb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Rosales?= Date: Sun, 12 Jan 2025 18:57:40 -0600 Subject: [PATCH 52/71] fix(rust/signed_doc): wip fixes to examples --- rust/signed_doc/Cargo.toml | 2 +- rust/signed_doc/examples/mk_signed_doc.rs | 52 ++++++++++++----------- 2 files changed, 28 insertions(+), 26 deletions(-) diff --git a/rust/signed_doc/Cargo.toml b/rust/signed_doc/Cargo.toml index 5a4d970346d..6129437edc6 100644 --- a/rust/signed_doc/Cargo.toml +++ b/rust/signed_doc/Cargo.toml @@ -12,7 +12,7 @@ workspace = true [dependencies] cardano-blockchain-types = { version = "0.0.1", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "v0.0.11" } -catalyst-types = { version = "0.0.1", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250111-00" } +catalyst-types = { version = "0.0.1", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250112-00" } anyhow = "1.0.95" serde = { version = "1.0.217", features = ["derive"] } serde_json = "1.0.134" diff --git a/rust/signed_doc/examples/mk_signed_doc.rs b/rust/signed_doc/examples/mk_signed_doc.rs index 1074d55e949..43bf1d49b45 100644 --- a/rust/signed_doc/examples/mk_signed_doc.rs +++ b/rust/signed_doc/examples/mk_signed_doc.rs @@ -81,7 +81,7 @@ fn decode_cbor_uuid(val: &coset::cbor::Value) -> anyhow::Result { Ok(uuid) } -fn encode_cbor_document_ref(doc_ref: &DocumentRef) -> coset::cbor::Value { +fn _encode_cbor_document_ref(doc_ref: &DocumentRef) -> coset::cbor::Value { match doc_ref { DocumentRef::Latest { id } => encode_cbor_uuid(&id.uuid()), DocumentRef::WithVer { id, ver } => { @@ -227,30 +227,32 @@ fn build_empty_cose_doc(doc_bytes: Vec, meta: &Metadata) -> coset::CoseSign coset::Label::Text("ver".to_string()), encode_cbor_uuid(&meta.doc_ver()), )); - if let Some(r#ref) = &meta.doc_ref() { - protected_header.rest.push(( - coset::Label::Text("ref".to_string()), - encode_cbor_document_ref(r#ref), - )); - } - if let Some(template) = &meta.doc_template() { - protected_header.rest.push(( - coset::Label::Text("template".to_string()), - encode_cbor_document_ref(template), - )); - } - if let Some(reply) = &meta.doc_reply() { - protected_header.rest.push(( - coset::Label::Text("reply".to_string()), - encode_cbor_document_ref(reply), - )); - } - if let Some(section) = &meta.doc_section() { - protected_header.rest.push(( - coset::Label::Text("section".to_string()), - coset::cbor::Value::Text(section.clone()), - )); - } + // WIP: encode additional metadata fields + // + // if let Some(r#ref) = &meta.doc_ref() { + // protected_header.rest.push(( + // coset::Label::Text("ref".to_string()), + // encode_cbor_document_ref(r#ref), + // )); + //} + // if let Some(template) = &meta.doc_template() { + // protected_header.rest.push(( + // coset::Label::Text("template".to_string()), + // encode_cbor_document_ref(template), + // )); + //} + // if let Some(reply) = &meta.doc_reply() { + // protected_header.rest.push(( + // coset::Label::Text("reply".to_string()), + // encode_cbor_document_ref(reply), + // )); + //} + // if let Some(section) = &meta.doc_section() { + // protected_header.rest.push(( + // coset::Label::Text("section".to_string()), + // coset::cbor::Value::Text(section.clone()), + // )); + //} coset::CoseSignBuilder::new() .protected(protected_header) From fbf4584cc02abf7a7952744ea742af6e1157a37b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Rosales?= Date: Sun, 12 Jan 2025 18:59:08 -0600 Subject: [PATCH 53/71] feat(rust/signed-doc): serialize DocumentRef to cbor value --- rust/signed_doc/src/metadata/document_ref.rs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/rust/signed_doc/src/metadata/document_ref.rs b/rust/signed_doc/src/metadata/document_ref.rs index ed540d4430f..737db88949a 100644 --- a/rust/signed_doc/src/metadata/document_ref.rs +++ b/rust/signed_doc/src/metadata/document_ref.rs @@ -1,4 +1,6 @@ //! Catalyst Signed Document Metadata. +use coset::cbor::Value; + use super::UuidV7; /// Reference to a Document. @@ -19,11 +21,22 @@ pub enum DocumentRef { }, } -impl TryFrom<&coset::cbor::Value> for DocumentRef { +impl From<&DocumentRef> for Value { + fn from(val: &DocumentRef) -> Self { + match val { + DocumentRef::Latest { id } => Value::from(*id), + DocumentRef::WithVer { id, ver } => { + Value::Array(vec![Value::from(*id), Value::from(*ver)]) + }, + } + } +} + +impl TryFrom<&Value> for DocumentRef { type Error = anyhow::Error; #[allow(clippy::indexing_slicing)] - fn try_from(val: &coset::cbor::Value) -> anyhow::Result { + fn try_from(val: &Value) -> anyhow::Result { if let Ok(id) = UuidV7::try_from(val) { Ok(DocumentRef::Latest { id }) } else { From b37fcb71256e57dc9bb6da9a8883072e10e9ae65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Rosales?= Date: Sun, 12 Jan 2025 19:00:38 -0600 Subject: [PATCH 54/71] fix(rust/signed-doc): add missing metadata aditional fields --- .../src/metadata/additional_fields.rs | 196 ++++++++++++++++-- 1 file changed, 180 insertions(+), 16 deletions(-) diff --git a/rust/signed_doc/src/metadata/additional_fields.rs b/rust/signed_doc/src/metadata/additional_fields.rs index 5612f1f1291..67c869764aa 100644 --- a/rust/signed_doc/src/metadata/additional_fields.rs +++ b/rust/signed_doc/src/metadata/additional_fields.rs @@ -1,14 +1,15 @@ //! Catalyst Signed Document Additional Fields. use anyhow::anyhow; +use coset::{cbor::Value, Label, ProtectedHeader}; -use super::{cose_protected_header_find, DocumentRef}; +use super::{cose_protected_header_find, DocumentRef, UuidV4}; /// Additional Metadata Fields. /// /// These values are extracted from the COSE Sign protected header labels. #[derive(Default, Debug, serde::Serialize, serde::Deserialize)] -pub(super) struct AdditionalFields { +pub struct AdditionalFields { /// Reference to the latest document. #[serde(rename = "ref")] pub(super) doc_ref: Option, @@ -18,18 +19,90 @@ pub(super) struct AdditionalFields { pub(super) reply: Option, /// Reference to the document section. pub(super) section: Option, + /// Reference to the document collaborators. Collaborator type is TBD. + pub(super) collabs: Option>, + /// Unique identifier for the brand that is running the voting. + pub(super) brand_id: Option, + /// Unique identifier for the campaign of voting. + pub(super) campaign_id: Option, + /// Unique identifier for the election. + pub(super) election_id: Option, + /// Unique identifier for the voting category as a collection of proposals. + pub(super) category_id: Option, } -impl TryFrom<&coset::ProtectedHeader> for AdditionalFields { +impl From<&AdditionalFields> for Vec<(Label, Value)> { + fn from(fields: &AdditionalFields) -> Self { + let mut vec = Vec::new(); + + if let Some(doc_ref) = &fields.doc_ref { + vec.push((Label::Text("ref".to_string()), doc_ref.into())); + } + + if let Some(template) = &fields.template { + vec.push((Label::Text("template".to_string()), template.into())); + } + + if let Some(reply) = &fields.reply { + vec.push((Label::Text("reply".to_string()), reply.into())); + } + + if let Some(section) = &fields.section { + vec.push(( + Label::Text("section".to_string()), + Value::Text(section.clone()), + )); + } + + if let Some(collabs) = &fields.collabs { + if !collabs.is_empty() { + vec.push(( + Label::Text("collabs".to_string()), + Value::Array(collabs.iter().cloned().map(Value::Text).collect()), + )); + } + } + + if let Some(brand_id) = &fields.brand_id { + vec.push((Label::Text("brand_id".to_string()), Value::from(*brand_id))); + } + + if let Some(campaign_id) = &fields.campaign_id { + vec.push(( + Label::Text("campaign_id".to_string()), + Value::from(*campaign_id), + )); + } + + if let Some(election_id) = &fields.election_id { + vec.push(( + Label::Text("election_id".to_string()), + Value::from(*election_id), + )); + } + + if let Some(category_id) = &fields.category_id { + vec.push(( + Label::Text("category_id".to_string()), + Value::from(*category_id), + )); + } + + vec + } +} + +impl TryFrom<&ProtectedHeader> for AdditionalFields { type Error = Vec; - fn try_from(protected: &coset::ProtectedHeader) -> Result { + #[allow(clippy::too_many_lines)] + fn try_from(protected: &ProtectedHeader) -> Result { let mut extra = AdditionalFields::default(); let mut errors = Vec::new(); - if let Some(cbor_doc_ref) = cose_protected_header_find(protected, |key| { - key == &coset::Label::Text("ref".to_string()) - }) { + if let Some(cbor_doc_ref) = + cose_protected_header_find(protected, |key| key == &Label::Text("ref".to_string())) + { match DocumentRef::try_from(cbor_doc_ref) { Ok(doc_ref) => { extra.doc_ref = Some(doc_ref); @@ -42,9 +115,9 @@ impl TryFrom<&coset::ProtectedHeader> for AdditionalFields { } } - if let Some(cbor_doc_template) = cose_protected_header_find(protected, |key| { - key == &coset::Label::Text("template".to_string()) - }) { + if let Some(cbor_doc_template) = + cose_protected_header_find(protected, |key| key == &Label::Text("template".to_string())) + { match DocumentRef::try_from(cbor_doc_template) { Ok(doc_template) => { extra.template = Some(doc_template); @@ -57,9 +130,9 @@ impl TryFrom<&coset::ProtectedHeader> for AdditionalFields { } } - if let Some(cbor_doc_reply) = cose_protected_header_find(protected, |key| { - key == &coset::Label::Text("reply".to_string()) - }) { + if let Some(cbor_doc_reply) = + cose_protected_header_find(protected, |key| key == &Label::Text("reply".to_string())) + { match DocumentRef::try_from(cbor_doc_reply) { Ok(doc_reply) => { extra.reply = Some(doc_reply); @@ -72,9 +145,9 @@ impl TryFrom<&coset::ProtectedHeader> for AdditionalFields { } } - if let Some(cbor_doc_section) = cose_protected_header_find(protected, |key| { - key == &coset::Label::Text("section".to_string()) - }) { + if let Some(cbor_doc_section) = + cose_protected_header_find(protected, |key| key == &Label::Text("section".to_string())) + { match cbor_doc_section.clone().into_text() { Ok(doc_section) => { extra.section = Some(doc_section); @@ -87,6 +160,97 @@ impl TryFrom<&coset::ProtectedHeader> for AdditionalFields { } } + if let Some(cbor_doc_collabs) = + cose_protected_header_find(protected, |key| key == &Label::Text("collabs".to_string())) + { + match cbor_doc_collabs.clone().into_array() { + Ok(collabs) => { + let mut c = Vec::new(); + for (ids, collaborator) in collabs.iter().cloned().enumerate() { + match collaborator.into_text() { + Ok(collaborator) => { + c.push(collaborator); + }, + Err(e) => { + errors.push(anyhow!( + "Invalid Collaborator at index {ids} of COSE protected header `collabs` field, err: {e:?}" + )); + }, + } + } + + if !c.is_empty() { + extra.collabs = Some(c); + } + }, + Err(e) => { + errors.push(anyhow!( + "Invalid COSE protected header `collabs` field, err: {e:?}" + )); + }, + } + } + + if let Some(cbor_doc_brand_id) = + cose_protected_header_find(protected, |key| key == &Label::Text("brand_id".to_string())) + { + match UuidV4::try_from(cbor_doc_brand_id) { + Ok(brand_id) => { + extra.brand_id = Some(brand_id); + }, + Err(e) => { + errors.push(anyhow!( + "Invalid COSE protected header `brand_id` field, err: {e}" + )); + }, + } + } + + if let Some(cbor_doc_campaign_id) = cose_protected_header_find(protected, |key| { + key == &Label::Text("campaign_id".to_string()) + }) { + match UuidV4::try_from(cbor_doc_campaign_id) { + Ok(campaign_id) => { + extra.campaign_id = Some(campaign_id); + }, + Err(e) => { + errors.push(anyhow!( + "Invalid COSE protected header `campaign_id` field, err: {e}" + )); + }, + } + } + + if let Some(cbor_doc_election_id) = cose_protected_header_find(protected, |key| { + key == &Label::Text("election_id".to_string()) + }) { + match UuidV4::try_from(cbor_doc_election_id) { + Ok(election_id) => { + extra.election_id = Some(election_id); + }, + Err(e) => { + errors.push(anyhow!( + "Invalid COSE protected header `election_id` field, err: {e}" + )); + }, + } + } + + if let Some(cbor_doc_category_id) = cose_protected_header_find(protected, |key| { + key == &Label::Text("category_id".to_string()) + }) { + match UuidV4::try_from(cbor_doc_category_id) { + Ok(category_id) => { + extra.category_id = Some(category_id); + }, + Err(e) => { + errors.push(anyhow!( + "Invalid COSE protected header `category_id` field, err: {e}" + )); + }, + } + } + if errors.is_empty() { Ok(extra) } else { From 4dd828bbaa0daff751218154f93d8b82db729b45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Rosales?= Date: Sun, 12 Jan 2025 19:16:10 -0600 Subject: [PATCH 55/71] fix(rust/signed-doc): encode metadata aditional fields as REST items --- rust/signed_doc/examples/mk_signed_doc.rs | 30 +++---------------- .../src/metadata/additional_fields.rs | 7 +++++ rust/signed_doc/src/metadata/mod.rs | 6 ++++ 3 files changed, 17 insertions(+), 26 deletions(-) diff --git a/rust/signed_doc/examples/mk_signed_doc.rs b/rust/signed_doc/examples/mk_signed_doc.rs index 43bf1d49b45..54c99ed1b16 100644 --- a/rust/signed_doc/examples/mk_signed_doc.rs +++ b/rust/signed_doc/examples/mk_signed_doc.rs @@ -227,33 +227,11 @@ fn build_empty_cose_doc(doc_bytes: Vec, meta: &Metadata) -> coset::CoseSign coset::Label::Text("ver".to_string()), encode_cbor_uuid(&meta.doc_ver()), )); - // WIP: encode additional metadata fields - // - // if let Some(r#ref) = &meta.doc_ref() { - // protected_header.rest.push(( - // coset::Label::Text("ref".to_string()), - // encode_cbor_document_ref(r#ref), - // )); - //} - // if let Some(template) = &meta.doc_template() { - // protected_header.rest.push(( - // coset::Label::Text("template".to_string()), - // encode_cbor_document_ref(template), - // )); - //} - // if let Some(reply) = &meta.doc_reply() { - // protected_header.rest.push(( - // coset::Label::Text("reply".to_string()), - // encode_cbor_document_ref(reply), - // )); - //} - // if let Some(section) = &meta.doc_section() { - // protected_header.rest.push(( - // coset::Label::Text("section".to_string()), - // coset::cbor::Value::Text(section.clone()), - // )); - //} + let meta_rest = meta.extra().header_rest(); + if !meta_rest.is_empty() { + protected_header.rest.extend(meta_rest); + } coset::CoseSignBuilder::new() .protected(protected_header) .payload(doc_bytes) diff --git a/rust/signed_doc/src/metadata/additional_fields.rs b/rust/signed_doc/src/metadata/additional_fields.rs index 67c869764aa..08b61b47ee0 100644 --- a/rust/signed_doc/src/metadata/additional_fields.rs +++ b/rust/signed_doc/src/metadata/additional_fields.rs @@ -31,6 +31,13 @@ pub struct AdditionalFields { pub(super) category_id: Option, } +impl AdditionalFields { + /// Returns the COSE Sign protected header REST fields. + pub fn header_rest(&self) -> Vec<(Label, Value)> { + self.into() + } +} + impl From<&AdditionalFields> for Vec<(Label, Value)> { fn from(fields: &AdditionalFields) -> Self { let mut vec = Vec::new(); diff --git a/rust/signed_doc/src/metadata/mod.rs b/rust/signed_doc/src/metadata/mod.rs index 143b1a7f97c..3cae59852f0 100644 --- a/rust/signed_doc/src/metadata/mod.rs +++ b/rust/signed_doc/src/metadata/mod.rs @@ -75,6 +75,12 @@ impl Metadata { pub fn content_encoding(&self) -> Option { self.content_encoding } + + /// Return reference to additional metadata fields. + #[must_use] + pub fn extra(&self) -> &AdditionalFields { + &self.extra + } } impl Display for Metadata { From 90d0622da1d5948429a8b36082b8893a87ee7231 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Rosales?= Date: Sun, 12 Jan 2025 20:41:32 -0600 Subject: [PATCH 56/71] fix(rust/signed-doc): handle validation errors with crate::error::Error --- rust/signed_doc/src/metadata/additional_fields.rs | 4 ++-- rust/signed_doc/src/metadata/mod.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/rust/signed_doc/src/metadata/additional_fields.rs b/rust/signed_doc/src/metadata/additional_fields.rs index 08b61b47ee0..ba0a62358a7 100644 --- a/rust/signed_doc/src/metadata/additional_fields.rs +++ b/rust/signed_doc/src/metadata/additional_fields.rs @@ -100,7 +100,7 @@ impl From<&AdditionalFields> for Vec<(Label, Value)> { } impl TryFrom<&ProtectedHeader> for AdditionalFields { - type Error = Vec; + type Error = crate::error::Error; #[allow(clippy::too_many_lines)] fn try_from(protected: &ProtectedHeader) -> Result { @@ -261,7 +261,7 @@ impl TryFrom<&ProtectedHeader> for AdditionalFields { if errors.is_empty() { Ok(extra) } else { - Err(errors) + Err(crate::error::Error(errors)) } } } diff --git a/rust/signed_doc/src/metadata/mod.rs b/rust/signed_doc/src/metadata/mod.rs index 3cae59852f0..bd98efdeea7 100644 --- a/rust/signed_doc/src/metadata/mod.rs +++ b/rust/signed_doc/src/metadata/mod.rs @@ -171,7 +171,7 @@ impl TryFrom<&coset::ProtectedHeader> for Metadata { let extra = AdditionalFields::try_from(protected).map_or_else( |e| { - errors.extend(e); + errors.extend(e.0); None }, Some, From 7a0fa43d5fb4e88a55e4318b43c5b8e159fe37b2 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Mon, 13 Jan 2025 12:21:38 +0200 Subject: [PATCH 57/71] remove author field --- rust/signed_doc/src/lib.rs | 17 +++++------------ rust/signed_doc/src/signature/mod.rs | 10 +++------- 2 files changed, 8 insertions(+), 19 deletions(-) diff --git a/rust/signed_doc/src/lib.rs b/rust/signed_doc/src/lib.rs index 8ebdf5aa349..e09c7f5f872 100644 --- a/rust/signed_doc/src/lib.rs +++ b/rust/signed_doc/src/lib.rs @@ -24,8 +24,6 @@ struct InnerCatalystSignedDocument { metadata: Metadata, /// Document Content content: Content, - /// Document Author - author: KidUri, /// Signatures signatures: Signatures, } @@ -74,17 +72,13 @@ impl TryFrom<&[u8]> for CatalystSignedDocument { }, Some, ); - let author = signatures.as_ref().and_then(|s| s.kids().first().cloned()); if cose_sign.payload.is_none() { errors.push(anyhow!("Document Content is missing")); } - if author.is_none() { - errors.push(anyhow!("Document Author is missing")); - } - match (cose_sign.payload, author, metadata, signatures) { - (Some(payload), Some(author), Some(metadata), Some(signatures)) => { + match (cose_sign.payload, metadata, signatures) { + (Some(payload), Some(metadata), Some(signatures)) => { let content = Content::new( payload, metadata.content_type(), @@ -99,7 +93,6 @@ impl TryFrom<&[u8]> for CatalystSignedDocument { inner: InnerCatalystSignedDocument { metadata, content, - author, signatures, } .into(), @@ -137,9 +130,9 @@ impl CatalystSignedDocument { &self.inner.content } - /// Return a Document's author + /// Return a Document's signatures #[must_use] - pub fn author(&self) -> KidUri { - self.inner.author.clone() + pub fn signatures(&self) -> &Signatures { + &self.inner.signatures } } diff --git a/rust/signed_doc/src/signature/mod.rs b/rust/signed_doc/src/signature/mod.rs index a53ce622e27..e4add4333b7 100644 --- a/rust/signed_doc/src/signature/mod.rs +++ b/rust/signed_doc/src/signature/mod.rs @@ -8,7 +8,8 @@ use coset::CoseSignature; pub struct Signature { /// Key ID kid: KidUri, - /// COSE Signature + /// COSE Signature + #[allow(dead_code)] signature: CoseSignature, } @@ -20,12 +21,6 @@ impl Signatures { pub fn kids(&self) -> Vec { self.0.iter().map(|sig| sig.kid.clone()).collect() } - - /// List of signatures. - #[allow(dead_code)] - pub fn signatures(&self) -> Vec { - self.0.iter().map(|sig| sig.signature.clone()).collect() - } } impl TryFrom<&Vec> for Signatures { @@ -48,6 +43,7 @@ impl TryFrom<&Vec> for Signatures { }, } }); + if errors.is_empty() { Err(errors.into()) } else { From e81df07f34659442d008237609b6569cdc9bc19d Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Tue, 14 Jan 2025 12:41:01 +0200 Subject: [PATCH 58/71] add doc_meta getter --- rust/signed_doc/src/lib.rs | 8 +++++++- rust/signed_doc/src/metadata/mod.rs | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/rust/signed_doc/src/lib.rs b/rust/signed_doc/src/lib.rs index e09c7f5f872..a06bcc8cc7c 100644 --- a/rust/signed_doc/src/lib.rs +++ b/rust/signed_doc/src/lib.rs @@ -14,7 +14,7 @@ mod error; mod metadata; mod signature; -pub use metadata::{DocumentRef, Metadata, UuidV7}; +pub use metadata::{AdditionalFields, DocumentRef, Metadata, UuidV7}; pub use signature::KidUri; use signature::Signatures; @@ -130,6 +130,12 @@ impl CatalystSignedDocument { &self.inner.content } + /// Return document metadata content. + #[must_use] + pub fn doc_meta(&self) -> &AdditionalFields { + self.inner.metadata.extra() + } + /// Return a Document's signatures #[must_use] pub fn signatures(&self) -> &Signatures { diff --git a/rust/signed_doc/src/metadata/mod.rs b/rust/signed_doc/src/metadata/mod.rs index bd98efdeea7..c9efa3bb993 100644 --- a/rust/signed_doc/src/metadata/mod.rs +++ b/rust/signed_doc/src/metadata/mod.rs @@ -9,7 +9,7 @@ mod document_ref; mod document_type; mod document_version; -use additional_fields::AdditionalFields; +pub use additional_fields::AdditionalFields; use anyhow::anyhow; pub use catalyst_types::uuid::{V4 as UuidV4, V7 as UuidV7}; pub use content_encoding::ContentEncoding; From 128a800d0161a68b86bc32d3991ca9f01e017a8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Rosales?= Date: Tue, 14 Jan 2025 19:41:58 -0600 Subject: [PATCH 59/71] fix(rust/signed_doc): use minicbor encode/decode * wip: fix examples --- rust/signed_doc/Cargo.toml | 3 +- rust/signed_doc/examples/mk_signed_doc.rs | 189 +++++++++--------- .../src/metadata/additional_fields.rs | 42 ++-- rust/signed_doc/src/metadata/document_ref.rs | 23 ++- rust/signed_doc/src/metadata/mod.rs | 42 +++- 5 files changed, 165 insertions(+), 134 deletions(-) diff --git a/rust/signed_doc/Cargo.toml b/rust/signed_doc/Cargo.toml index 6129437edc6..d0ac66d79b4 100644 --- a/rust/signed_doc/Cargo.toml +++ b/rust/signed_doc/Cargo.toml @@ -12,13 +12,14 @@ workspace = true [dependencies] cardano-blockchain-types = { version = "0.0.1", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "v0.0.11" } -catalyst-types = { version = "0.0.1", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250112-00" } +catalyst-types = { version = "0.0.1", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250114-01" } anyhow = "1.0.95" serde = { version = "1.0.217", features = ["derive"] } serde_json = "1.0.134" # TODO: Bump this to the latest version and fix the code jsonschema = "0.18.3" coset = "0.3.8" +minicbor = "0.25.1" brotli = "7.0.0" ed25519-dalek = { version = "2.1.1", features = ["pem", "rand_core"] } uuid = { version = "1.11.0", features = ["v4", "v7", "serde"] } diff --git a/rust/signed_doc/examples/mk_signed_doc.rs b/rust/signed_doc/examples/mk_signed_doc.rs index 54c99ed1b16..7f981683e6a 100644 --- a/rust/signed_doc/examples/mk_signed_doc.rs +++ b/rust/signed_doc/examples/mk_signed_doc.rs @@ -8,7 +8,7 @@ use std::{ path::PathBuf, }; -use catalyst_signed_doc::{DocumentRef, KidUri, Metadata, UuidV7}; +use catalyst_signed_doc::{DocumentRef, KidUri, Metadata}; use clap::Parser; use coset::{iana::CoapContentFormat, CborSerializable}; use ed25519_dalek::{ @@ -68,7 +68,7 @@ fn encode_cbor_uuid(uuid: &uuid::Uuid) -> coset::cbor::Value { ) } -fn decode_cbor_uuid(val: &coset::cbor::Value) -> anyhow::Result { +fn _decode_cbor_uuid(val: &coset::cbor::Value) -> anyhow::Result { let Some((UUID_CBOR_TAG, coset::cbor::Value::Bytes(bytes))) = val.as_tag() else { anyhow::bail!("Invalid CBOR encoded UUID type"); }; @@ -94,18 +94,8 @@ fn _encode_cbor_document_ref(doc_ref: &DocumentRef) -> coset::cbor::Value { } #[allow(clippy::indexing_slicing)] -fn decode_cbor_document_ref(val: &coset::cbor::Value) -> anyhow::Result { - if let Ok(id) = UuidV7::try_from(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 = UuidV7::try_from(&array[0])?; - let ver = UuidV7::try_from(&array[1])?; - Ok(DocumentRef::WithVer { id, ver }) - } +fn _decode_cbor_document_ref(val: &coset::cbor::Value) -> anyhow::Result { + DocumentRef::try_from(val) } impl Cli { @@ -227,7 +217,7 @@ fn build_empty_cose_doc(doc_bytes: Vec, meta: &Metadata) -> coset::CoseSign coset::Label::Text("ver".to_string()), encode_cbor_uuid(&meta.doc_ver()), )); - let meta_rest = meta.extra().header_rest(); + let meta_rest = meta.extra().header_rest().unwrap_or_default(); if !meta_rest.is_empty() { protected_header.rest.extend(meta_rest); @@ -336,89 +326,92 @@ fn validate_cose_protected_header(cose: &coset::CoseSign) -> anyhow::Result<()> "Invalid COSE document protected header" ); - 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"); - }; - 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" - ); - } + // 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"); + // }; + // 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(()) } diff --git a/rust/signed_doc/src/metadata/additional_fields.rs b/rust/signed_doc/src/metadata/additional_fields.rs index ba0a62358a7..9ea5b249245 100644 --- a/rust/signed_doc/src/metadata/additional_fields.rs +++ b/rust/signed_doc/src/metadata/additional_fields.rs @@ -3,7 +3,7 @@ use anyhow::anyhow; use coset::{cbor::Value, Label, ProtectedHeader}; -use super::{cose_protected_header_find, DocumentRef, UuidV4}; +use super::{cose_protected_header_find, decode_cbor_uuid, encode_cbor_value, DocumentRef, UuidV4}; /// Additional Metadata Fields. /// @@ -33,25 +33,30 @@ pub struct AdditionalFields { impl AdditionalFields { /// Returns the COSE Sign protected header REST fields. - pub fn header_rest(&self) -> Vec<(Label, Value)> { - self.into() + /// + /// # Errors + /// If any internal field cannot be converted into `Value`. + pub fn header_rest(&self) -> anyhow::Result> { + self.try_into() } } -impl From<&AdditionalFields> for Vec<(Label, Value)> { - fn from(fields: &AdditionalFields) -> Self { +impl TryFrom<&AdditionalFields> for Vec<(Label, Value)> { + type Error = anyhow::Error; + + fn try_from(fields: &AdditionalFields) -> anyhow::Result { let mut vec = Vec::new(); if let Some(doc_ref) = &fields.doc_ref { - vec.push((Label::Text("ref".to_string()), doc_ref.into())); + vec.push((Label::Text("ref".to_string()), doc_ref.try_into()?)); } if let Some(template) = &fields.template { - vec.push((Label::Text("template".to_string()), template.into())); + vec.push((Label::Text("template".to_string()), template.try_into()?)); } if let Some(reply) = &fields.reply { - vec.push((Label::Text("reply".to_string()), reply.into())); + vec.push((Label::Text("reply".to_string()), reply.try_into()?)); } if let Some(section) = &fields.section { @@ -71,31 +76,34 @@ impl From<&AdditionalFields> for Vec<(Label, Value)> { } if let Some(brand_id) = &fields.brand_id { - vec.push((Label::Text("brand_id".to_string()), Value::from(*brand_id))); + vec.push(( + Label::Text("brand_id".to_string()), + encode_cbor_value(brand_id)?, + )); } if let Some(campaign_id) = &fields.campaign_id { vec.push(( Label::Text("campaign_id".to_string()), - Value::from(*campaign_id), + encode_cbor_value(campaign_id)?, )); } if let Some(election_id) = &fields.election_id { vec.push(( Label::Text("election_id".to_string()), - Value::from(*election_id), + encode_cbor_value(election_id)?, )); } if let Some(category_id) = &fields.category_id { vec.push(( Label::Text("category_id".to_string()), - Value::from(*category_id), + encode_cbor_value(*category_id)?, )); } - vec + Ok(vec) } } @@ -201,7 +209,7 @@ impl TryFrom<&ProtectedHeader> for AdditionalFields { if let Some(cbor_doc_brand_id) = cose_protected_header_find(protected, |key| key == &Label::Text("brand_id".to_string())) { - match UuidV4::try_from(cbor_doc_brand_id) { + match decode_cbor_uuid(cbor_doc_brand_id.clone()) { Ok(brand_id) => { extra.brand_id = Some(brand_id); }, @@ -216,7 +224,7 @@ impl TryFrom<&ProtectedHeader> for AdditionalFields { if let Some(cbor_doc_campaign_id) = cose_protected_header_find(protected, |key| { key == &Label::Text("campaign_id".to_string()) }) { - match UuidV4::try_from(cbor_doc_campaign_id) { + match decode_cbor_uuid(cbor_doc_campaign_id.clone()) { Ok(campaign_id) => { extra.campaign_id = Some(campaign_id); }, @@ -231,7 +239,7 @@ impl TryFrom<&ProtectedHeader> for AdditionalFields { if let Some(cbor_doc_election_id) = cose_protected_header_find(protected, |key| { key == &Label::Text("election_id".to_string()) }) { - match UuidV4::try_from(cbor_doc_election_id) { + match decode_cbor_uuid(cbor_doc_election_id.clone()) { Ok(election_id) => { extra.election_id = Some(election_id); }, @@ -246,7 +254,7 @@ impl TryFrom<&ProtectedHeader> for AdditionalFields { if let Some(cbor_doc_category_id) = cose_protected_header_find(protected, |key| { key == &Label::Text("category_id".to_string()) }) { - match UuidV4::try_from(cbor_doc_category_id) { + match decode_cbor_uuid(cbor_doc_category_id.clone()) { Ok(category_id) => { extra.category_id = Some(category_id); }, diff --git a/rust/signed_doc/src/metadata/document_ref.rs b/rust/signed_doc/src/metadata/document_ref.rs index 737db88949a..fc4de6ea10e 100644 --- a/rust/signed_doc/src/metadata/document_ref.rs +++ b/rust/signed_doc/src/metadata/document_ref.rs @@ -1,7 +1,7 @@ //! Catalyst Signed Document Metadata. use coset::cbor::Value; -use super::UuidV7; +use super::{decode_cbor_uuid, encode_cbor_value, UuidV7}; /// Reference to a Document. #[derive(Copy, Clone, Debug, serde::Serialize, serde::Deserialize)] @@ -21,12 +21,17 @@ pub enum DocumentRef { }, } -impl From<&DocumentRef> for Value { - fn from(val: &DocumentRef) -> Self { - match val { - DocumentRef::Latest { id } => Value::from(*id), +impl TryFrom<&DocumentRef> for Value { + type Error = anyhow::Error; + + fn try_from(value: &DocumentRef) -> Result { + match value { + DocumentRef::Latest { id } => encode_cbor_value(id), DocumentRef::WithVer { id, ver } => { - Value::Array(vec![Value::from(*id), Value::from(*ver)]) + Ok(Value::Array(vec![ + encode_cbor_value(id)?, + encode_cbor_value(ver)?, + ])) }, } } @@ -37,7 +42,7 @@ impl TryFrom<&Value> for DocumentRef { #[allow(clippy::indexing_slicing)] fn try_from(val: &Value) -> anyhow::Result { - if let Ok(id) = UuidV7::try_from(val) { + if let Ok(id) = decode_cbor_uuid(val.clone()) { Ok(DocumentRef::Latest { id }) } else { let Some(array) = val.as_array() else { @@ -47,8 +52,8 @@ impl TryFrom<&Value> for DocumentRef { array.len() == 2, "Document Reference array of two UUIDs was expected" ); - let id = UuidV7::try_from(&array[0])?; - let ver = UuidV7::try_from(&array[1])?; + let id = decode_cbor_uuid(array[0].clone())?; + let ver = decode_cbor_uuid(array[1].clone())?; anyhow::ensure!( ver >= id, "Document Reference Version can never be smaller than its ID" diff --git a/rust/signed_doc/src/metadata/mod.rs b/rust/signed_doc/src/metadata/mod.rs index c9efa3bb993..e4b951338c5 100644 --- a/rust/signed_doc/src/metadata/mod.rs +++ b/rust/signed_doc/src/metadata/mod.rs @@ -14,6 +14,7 @@ use anyhow::anyhow; pub use catalyst_types::uuid::{V4 as UuidV4, V7 as UuidV7}; pub use content_encoding::ContentEncoding; pub use content_type::ContentType; +use coset::CborSerializable; pub use document_id::DocumentId; pub use document_ref::DocumentRef; pub use document_type::DocumentType; @@ -129,13 +130,13 @@ impl TryFrom<&coset::ProtectedHeader> for Metadata { )); } - let mut doc_type = None; + let mut doc_type: Option = None; if let Some(value) = cose_protected_header_find(protected, |key| { key == &coset::Label::Text("type".to_string()) }) { - match UuidV4::try_from(value) { + match decode_cbor_uuid(value.clone()) { Ok(uuid) => doc_type = Some(uuid), - Err(e) => errors.push(anyhow!("Document `type` is invalid: {e}")), + Err(e) => errors.push(anyhow!("Invalid document type UUID: {e}")), } } else { errors.push(anyhow!( @@ -143,25 +144,25 @@ impl TryFrom<&coset::ProtectedHeader> for Metadata { )); } - let mut id = None; + let mut id: Option = None; if let Some(value) = cose_protected_header_find(protected, |key| { key == &coset::Label::Text("id".to_string()) }) { - match UuidV7::try_from(value) { + match decode_cbor_uuid(value.clone()) { Ok(uuid) => id = Some(uuid), - Err(e) => errors.push(anyhow!("Document `id` is invalid: {e}")), + Err(e) => errors.push(anyhow!("Invalid document ID UUID: {e}")), } } else { errors.push(anyhow!("Invalid COSE protected header, missing `id` field")); } - let mut ver = None; + let mut ver: Option = None; if let Some(value) = cose_protected_header_find(protected, |key| { key == &coset::Label::Text("ver".to_string()) }) { - match UuidV7::try_from(value) { + match decode_cbor_uuid(value.clone()) { Ok(uuid) => ver = Some(uuid), - Err(e) => errors.push(anyhow!("Document `ver` is invalid: {e}")), + Err(e) => errors.push(anyhow!("Invalid document version UUID: {e}")), } } else { errors.push(anyhow!( @@ -218,3 +219,26 @@ fn cose_protected_header_find( .find(|(key, _)| predicate(key)) .map(|(_, value)| value) } + +/// Convert from `minicbor` into `coset::cbor::Value`. +pub(crate) fn encode_cbor_value>( + value: T, +) -> anyhow::Result { + let mut cbor_bytes = Vec::new(); + minicbor::encode(value, &mut cbor_bytes) + .map_err(|e| anyhow::anyhow!("Unable to encode CBOR value, err: {e}"))?; + coset::cbor::Value::from_slice(&cbor_bytes) + .map_err(|e| anyhow::anyhow!("Invalid CBOR value, err: {e}")) +} + +/// Convert `coset::cbor::Value` into `UuidV4`. +pub(crate) fn decode_cbor_uuid minicbor::decode::Decode<'a, ()> + From>( + value: coset::cbor::Value, +) -> anyhow::Result { + match value.to_vec() { + Ok(cbor_value) => { + minicbor::decode(&cbor_value).map_err(|e| anyhow!("Invalid UUID, err: {e}")) + }, + Err(e) => anyhow::bail!("Invalid CBOR value, err: {e}"), + } +} From 28369b2f7d93b8c406ddc405519504621b0552fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Rosales?= Date: Tue, 14 Jan 2025 20:47:15 -0600 Subject: [PATCH 60/71] fix(rust/signed-doc): remove unused dependencies --- rust/signed_doc/Cargo.toml | 3 --- 1 file changed, 3 deletions(-) diff --git a/rust/signed_doc/Cargo.toml b/rust/signed_doc/Cargo.toml index d0ac66d79b4..5a554c3a3fc 100644 --- a/rust/signed_doc/Cargo.toml +++ b/rust/signed_doc/Cargo.toml @@ -11,7 +11,6 @@ license.workspace = true workspace = true [dependencies] -cardano-blockchain-types = { version = "0.0.1", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "v0.0.11" } catalyst-types = { version = "0.0.1", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250114-01" } anyhow = "1.0.95" serde = { version = "1.0.217", features = ["derive"] } @@ -24,9 +23,7 @@ brotli = "7.0.0" ed25519-dalek = { version = "2.1.1", features = ["pem", "rand_core"] } uuid = { version = "1.11.0", features = ["v4", "v7", "serde"] } hex = "0.4.3" -fluent-uri = "0.3.2" thiserror = "2.0.9" -base64-url = "3.0.0" [dev-dependencies] clap = { version = "4.5.23", features = ["derive", "env"] } From 2b68bddfcdf61f4780bfd9aad0bfee20142d20fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Rosales?= Date: Tue, 14 Jan 2025 20:48:02 -0600 Subject: [PATCH 61/71] chore(docs): update spelling dictionary --- .config/dictionaries/project.dic | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.config/dictionaries/project.dic b/.config/dictionaries/project.dic index d4551639c24..4e80f2a81b4 100644 --- a/.config/dictionaries/project.dic +++ b/.config/dictionaries/project.dic @@ -123,6 +123,7 @@ jorm jormungandr Jörmungandr jsonschema +kiduri lcov Leay Leshiy @@ -238,6 +239,7 @@ smac stevenj stringzilla subsec +subnetwork symlinkat syscall tacho From 7d1537a231b1f8f01605a6987fd32095962134d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Rosales?= Date: Tue, 14 Jan 2025 21:26:35 -0600 Subject: [PATCH 62/71] fix(rust/signed-doc): update catalyst-types --- rust/signed_doc/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/signed_doc/Cargo.toml b/rust/signed_doc/Cargo.toml index 5a554c3a3fc..7502edfaf80 100644 --- a/rust/signed_doc/Cargo.toml +++ b/rust/signed_doc/Cargo.toml @@ -11,7 +11,7 @@ license.workspace = true workspace = true [dependencies] -catalyst-types = { version = "0.0.1", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250114-01" } +catalyst-types = { version = "0.0.1", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250114-02" } anyhow = "1.0.95" serde = { version = "1.0.217", features = ["derive"] } serde_json = "1.0.134" From 1254bb48937ad97fd35d8bafb96e7d676340acdd Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Wed, 15 Jan 2025 12:37:00 +0200 Subject: [PATCH 63/71] update CatalystSignedDocument decoding --- rust/signed_doc/src/lib.rs | 102 ++++++++++++++++++++----------------- 1 file changed, 54 insertions(+), 48 deletions(-) diff --git a/rust/signed_doc/src/lib.rs b/rust/signed_doc/src/lib.rs index a06bcc8cc7c..ecc0562c437 100644 --- a/rust/signed_doc/src/lib.rs +++ b/rust/signed_doc/src/lib.rs @@ -48,13 +48,59 @@ impl Display for CatalystSignedDocument { } } -impl TryFrom<&[u8]> for CatalystSignedDocument { - type Error = error::Error; +impl CatalystSignedDocument { + // A bunch of getters to access the contents, or reason through the document, such as. + + /// Return Document Type `UUIDv4`. + #[must_use] + pub fn doc_type(&self) -> uuid::Uuid { + self.inner.metadata.doc_type() + } + + /// Return Document ID `UUIDv7`. + #[must_use] + pub fn doc_id(&self) -> uuid::Uuid { + self.inner.metadata.doc_id() + } + + /// Return Document Version `UUIDv7`. + #[must_use] + pub fn doc_ver(&self) -> uuid::Uuid { + self.inner.metadata.doc_ver() + } + + /// Return document `Content`. + #[must_use] + pub fn doc_content(&self) -> &Content { + &self.inner.content + } + + /// Return document metadata content. + #[must_use] + pub fn doc_meta(&self) -> &AdditionalFields { + self.inner.metadata.extra() + } + + /// Return a Document's signatures + #[must_use] + pub fn signatures(&self) -> &Signatures { + &self.inner.signatures + } +} + +impl minicbor::Decode<'_, ()> for CatalystSignedDocument { + fn decode(d: &mut minicbor::Decoder<'_>, (): &mut ()) -> Result { + let start = d.position(); + d.skip()?; + let end = d.position(); + let cose_bytes = d + .input() + .get(start..end) + .ok_or(minicbor::decode::Error::end_of_input())?; - fn try_from(cose_bytes: &[u8]) -> Result { - // Try reading as a tagged COSE SIGN, otherwise try reading as untagged. - let cose_sign = coset::CoseSign::from_slice(cose_bytes) - .map_err(|e| vec![anyhow::anyhow!("Invalid COSE Sign document: {e}")])?; + let cose_sign = coset::CoseSign::from_slice(cose_bytes).map_err(|e| { + minicbor::decode::Error::message(format!("Invalid COSE Sign document: {e}")) + })?; let mut errors = Vec::new(); @@ -86,7 +132,7 @@ impl TryFrom<&[u8]> for CatalystSignedDocument { ) .map_err(|e| { errors.push(anyhow!("Invalid Document Content: {e}")); - errors + minicbor::decode::Error::custom(error::Error(errors)) })?; Ok(CatalystSignedDocument { @@ -98,47 +144,7 @@ impl TryFrom<&[u8]> for CatalystSignedDocument { .into(), }) }, - _ => Err(error::Error(errors)), + _ => Err(minicbor::decode::Error::custom(error::Error(errors))), } } } - -impl CatalystSignedDocument { - // A bunch of getters to access the contents, or reason through the document, such as. - - /// Return Document Type `UUIDv4`. - #[must_use] - pub fn doc_type(&self) -> uuid::Uuid { - self.inner.metadata.doc_type() - } - - /// Return Document ID `UUIDv7`. - #[must_use] - pub fn doc_id(&self) -> uuid::Uuid { - self.inner.metadata.doc_id() - } - - /// Return Document Version `UUIDv7`. - #[must_use] - pub fn doc_ver(&self) -> uuid::Uuid { - self.inner.metadata.doc_ver() - } - - /// Return document `Content`. - #[must_use] - pub fn doc_content(&self) -> &Content { - &self.inner.content - } - - /// Return document metadata content. - #[must_use] - pub fn doc_meta(&self) -> &AdditionalFields { - self.inner.metadata.extra() - } - - /// Return a Document's signatures - #[must_use] - pub fn signatures(&self) -> &Signatures { - &self.inner.signatures - } -} From 723dbe4f5f3194b6ca160c2bd33e7f3426d5543c Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Wed, 15 Jan 2025 13:21:24 +0200 Subject: [PATCH 64/71] fix --- rust/signed_doc/examples/cat-signed-doc.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/rust/signed_doc/examples/cat-signed-doc.rs b/rust/signed_doc/examples/cat-signed-doc.rs index 20fe9c2b1dd..1fb416fede2 100644 --- a/rust/signed_doc/examples/cat-signed-doc.rs +++ b/rust/signed_doc/examples/cat-signed-doc.rs @@ -13,6 +13,7 @@ use std::{ use catalyst_signed_doc::CatalystSignedDocument; use clap::Parser; +use minicbor::Decode; /// Hermes cli commands #[derive(clap::Parser)] @@ -42,7 +43,9 @@ impl Cli { Self::InspectBytes { cose_sign_str } => hex::decode(&cose_sign_str)?, }; println!("Bytes read:\n{}\n", hex::encode(&cose_bytes)); - let cat_signed_doc: CatalystSignedDocument = cose_bytes.as_slice().try_into()?; + + let cat_signed_doc = + CatalystSignedDocument::decode(&mut minicbor::Decoder::new(&cose_bytes), &mut ())?; println!("{cat_signed_doc}"); Ok(()) } From ebd29d13696688cc3a8b1242351080a0f3475227 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Wed, 15 Jan 2025 16:15:12 +0200 Subject: [PATCH 65/71] make minicbor imports public --- rust/signed_doc/src/lib.rs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/rust/signed_doc/src/lib.rs b/rust/signed_doc/src/lib.rs index ecc0562c437..d22d2b080e6 100644 --- a/rust/signed_doc/src/lib.rs +++ b/rust/signed_doc/src/lib.rs @@ -1,4 +1,10 @@ //! Catalyst documents signing crate + +mod content; +mod error; +mod metadata; +mod signature; + use std::{ convert::TryFrom, fmt::{Display, Formatter}, @@ -8,13 +14,8 @@ use std::{ use anyhow::anyhow; use content::Content; use coset::CborSerializable; - -mod content; -mod error; -mod metadata; -mod signature; - pub use metadata::{AdditionalFields, DocumentRef, Metadata, UuidV7}; +pub use minicbor::{decode, Decode, Decoder}; pub use signature::KidUri; use signature::Signatures; @@ -88,8 +89,8 @@ impl CatalystSignedDocument { } } -impl minicbor::Decode<'_, ()> for CatalystSignedDocument { - fn decode(d: &mut minicbor::Decoder<'_>, (): &mut ()) -> Result { +impl Decode<'_, ()> for CatalystSignedDocument { + fn decode(d: &mut Decoder<'_>, (): &mut ()) -> Result { let start = d.position(); d.skip()?; let end = d.position(); From 3cfc3296903b48dfefd40aa495de576ea771670f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Rosales?= Date: Wed, 15 Jan 2025 13:48:43 -0600 Subject: [PATCH 66/71] fix(rust/signed_doc): cleanup examples, fix signatures decoding --- rust/signed_doc/examples/cat-signed-doc.rs | 60 ---- rust/signed_doc/examples/mk_signed_doc.rs | 268 +++--------------- rust/signed_doc/src/error.rs | 18 +- rust/signed_doc/src/lib.rs | 8 +- .../src/metadata/additional_fields.rs | 2 +- rust/signed_doc/src/metadata/mod.rs | 6 +- rust/signed_doc/src/signature/mod.rs | 5 +- 7 files changed, 68 insertions(+), 299 deletions(-) delete mode 100644 rust/signed_doc/examples/cat-signed-doc.rs diff --git a/rust/signed_doc/examples/cat-signed-doc.rs b/rust/signed_doc/examples/cat-signed-doc.rs deleted file mode 100644 index 1fb416fede2..00000000000 --- a/rust/signed_doc/examples/cat-signed-doc.rs +++ /dev/null @@ -1,60 +0,0 @@ -//! Inspect a Catalyst Signed Document. -use std::{ - fs::{ - // read_to_string, - File, - }, - io::{ - Read, - // Write - }, - path::PathBuf, -}; - -use catalyst_signed_doc::CatalystSignedDocument; -use clap::Parser; -use minicbor::Decode; - -/// 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_path: PathBuf, - }, - /// Inspect COSE document hex-formatted bytes - InspectBytes { - /// Hex-formatted COSE SIGN Bytes - cose_sign_str: String, - }, -} - -impl Cli { - /// Execute Cli command - fn exec(self) -> anyhow::Result<()> { - let cose_bytes = match self { - Self::Inspect { cose_sign_path } => { - let mut cose_file = File::open(cose_sign_path)?; - let mut cose_file_bytes = Vec::new(); - cose_file.read_to_end(&mut cose_file_bytes)?; - cose_file_bytes - }, - Self::InspectBytes { cose_sign_str } => hex::decode(&cose_sign_str)?, - }; - println!("Bytes read:\n{}\n", hex::encode(&cose_bytes)); - - let cat_signed_doc = - CatalystSignedDocument::decode(&mut minicbor::Decoder::new(&cose_bytes), &mut ())?; - println!("{cat_signed_doc}"); - Ok(()) - } -} - -fn main() { - println!("Catalyst Signed Document"); - println!("------------------------"); - if let Err(err) = Cli::parse().exec() { - println!("{err}"); - } -} diff --git a/rust/signed_doc/examples/mk_signed_doc.rs b/rust/signed_doc/examples/mk_signed_doc.rs index 7f981683e6a..2ee994efef1 100644 --- a/rust/signed_doc/examples/mk_signed_doc.rs +++ b/rust/signed_doc/examples/mk_signed_doc.rs @@ -8,13 +8,10 @@ use std::{ path::PathBuf, }; -use catalyst_signed_doc::{DocumentRef, KidUri, Metadata}; +use catalyst_signed_doc::{CatalystSignedDocument, Decode, Decoder, DocumentRef, KidUri, Metadata}; use clap::Parser; use coset::{iana::CoapContentFormat, CborSerializable}; -use ed25519_dalek::{ - ed25519::signature::Signer, - pkcs8::{DecodePrivateKey, DecodePublicKey}, -}; +use ed25519_dalek::{ed25519::signature::Signer, pkcs8::DecodePrivateKey}; fn main() { if let Err(err) = Cli::parse().exec() { @@ -44,21 +41,21 @@ enum Cli { /// This exact file would be modified and new signature would be added doc: PathBuf, /// Signer kid - kid: String, + kid: KidUri, }, - /// Verifies COSE document - Verify { - /// Path to the public key in PEM format - pk: PathBuf, + /// Inspects Catalyst Signed Document + Inspect { /// Path to the fully formed (should has at least one signature) COSE document - doc: PathBuf, - /// Path to the json schema (Draft 7) to validate document against it - schema: PathBuf, + path: PathBuf, + }, + /// Inspects Catalyst Signed Document from hex-encoded bytes + InspectBytes { + /// Hex-formatted COSE SIGN Bytes + cose_sign_hex: String, }, } const CONTENT_ENCODING_KEY: &str = "Content-Encoding"; -const CONTENT_ENCODING_VALUE: &str = "br"; const UUID_CBOR_TAG: u64 = 37; fn encode_cbor_uuid(uuid: &uuid::Uuid) -> coset::cbor::Value { @@ -68,31 +65,6 @@ fn encode_cbor_uuid(uuid: &uuid::Uuid) -> coset::cbor::Value { ) } -fn _decode_cbor_uuid(val: &coset::cbor::Value) -> anyhow::Result { - 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) -} - -fn _encode_cbor_document_ref(doc_ref: &DocumentRef) -> coset::cbor::Value { - match doc_ref { - DocumentRef::Latest { id } => encode_cbor_uuid(&id.uuid()), - DocumentRef::WithVer { id, ver } => { - coset::cbor::Value::Array(vec![ - encode_cbor_uuid(&id.uuid()), - encode_cbor_uuid(&ver.uuid()), - ]) - }, - } -} - #[allow(clippy::indexing_slicing)] fn _decode_cbor_document_ref(val: &coset::cbor::Value) -> anyhow::Result { DocumentRef::try_from(val) @@ -118,21 +90,22 @@ impl Cli { store_cose_file(empty_cose_sign, &output)?; }, Self::Sign { sk, doc, kid } => { - let sk = load_secret_key_from_file(&sk)?; - let mut cose = load_cose_from_file(&doc)?; - add_signature_to_cose(&mut cose, &sk, kid); + let sk = load_secret_key_from_file(&sk) + .map_err(|e| anyhow::anyhow!("Failed to load SK FILE: {e}"))?; + let mut cose = load_cose_from_file(&doc) + .map_err(|e| anyhow::anyhow!("Failed to load COSE FROM FILE: {e}"))?; + add_signature_to_cose(&mut cose, &sk, kid.to_string()); store_cose_file(cose, &doc)?; }, - Self::Verify { pk, doc, schema } => { - let pk = load_public_key_from_file(&pk) - .map_err(|e| anyhow::anyhow!("Failed to load public key from file: {e}"))?; - let schema = load_schema_from_file(&schema).map_err(|e| { - anyhow::anyhow!("Failed to load document schema from file: {e}") - })?; - let cose = load_cose_from_file(&doc) - .map_err(|e| anyhow::anyhow!("Failed to load COSE SIGN from file: {e}"))?; - validate_cose(&cose, &pk, &schema)?; - println!("Document is valid."); + Self::Inspect { path } => { + let mut cose_file = File::open(path)?; + let mut cose_bytes = Vec::new(); + cose_file.read_to_end(&mut cose_bytes)?; + decode_signed_doc(&cose_bytes); + }, + Self::InspectBytes { cose_sign_hex } => { + let cose_bytes = hex::decode(&cose_sign_hex)?; + decode_signed_doc(&cose_bytes); }, } println!("Done"); @@ -140,6 +113,21 @@ impl Cli { } } +fn decode_signed_doc(cose_bytes: &[u8]) { + println!( + "Decoding {} bytes: {}", + cose_bytes.len(), + hex::encode(cose_bytes) + ); + match CatalystSignedDocument::decode(&mut Decoder::new(cose_bytes), &mut ()) { + Ok(cat_signed_doc) => { + println!("This is a valid Catalyst Signed Document."); + println!("{cat_signed_doc}"); + }, + Err(e) => eprintln!("Invalid Cataylyst Signed Document, err: {e}"), + } +} + fn load_schema_from_file(schema_path: &PathBuf) -> anyhow::Result { let schema_file = File::open(schema_path)?; let schema_json = serde_json::from_reader(schema_file)?; @@ -176,23 +164,6 @@ fn brotli_compress_json(doc: &serde_json::Value) -> anyhow::Result> { Ok(buf) } -fn brotli_decompress_json(mut doc_bytes: &[u8]) -> anyhow::Result { - let mut buf = Vec::new(); - brotli::BrotliDecompress(&mut doc_bytes, &mut buf)?; - let json_doc = serde_json::from_slice(&buf)?; - Ok(json_doc) -} - -fn cose_protected_header() -> coset::Header { - coset::HeaderBuilder::new() - .content_format(CoapContentFormat::Json) - .text_value( - CONTENT_ENCODING_KEY.to_string(), - CONTENT_ENCODING_VALUE.to_string().into(), - ) - .build() -} - fn build_empty_cose_doc(doc_bytes: Vec, meta: &Metadata) -> coset::CoseSign { let mut builder = coset::HeaderBuilder::new().content_format(CoapContentFormat::from(meta.content_type())); @@ -238,7 +209,9 @@ fn load_cose_from_file(cose_path: &PathBuf) -> anyhow::Result { fn store_cose_file(cose: coset::CoseSign, output: &PathBuf) -> anyhow::Result<()> { let mut cose_file = File::create(output)?; - let cose_bytes = cose.to_vec().map_err(|e| anyhow::anyhow!("{e}"))?; + let cose_bytes = cose + .to_vec() + .map_err(|e| anyhow::anyhow!("Failed to Store COSE SIGN: {e}"))?; cose_file.write_all(&cose_bytes)?; Ok(()) } @@ -249,12 +222,6 @@ fn load_secret_key_from_file(sk_path: &PathBuf) -> anyhow::Result anyhow::Result { - let pk_str = read_to_string(pk_path)?; - let pk = ed25519_dalek::VerifyingKey::from_public_key_pem(&pk_str)?; - Ok(pk) -} - fn add_signature_to_cose(cose: &mut coset::CoseSign, sk: &ed25519_dalek::SigningKey, kid: String) { let protected_header = coset::HeaderBuilder::new() .key_id(kid.into_bytes()) @@ -266,152 +233,3 @@ fn add_signature_to_cose(cose: &mut coset::CoseSign, sk: &ed25519_dalek::Signing signature.signature = sk.sign(&data_to_sign).to_vec(); cose.signatures.push(signature); } - -fn validate_cose( - cose: &coset::CoseSign, pk: &ed25519_dalek::VerifyingKey, schema: &jsonschema::JSONSchema, -) -> anyhow::Result<()> { - validate_cose_protected_header(cose)?; - - let Some(payload) = &cose.payload else { - anyhow::bail!("COSE missing payload field with the JSON content in it"); - }; - let json_doc = brotli_decompress_json(payload.as_slice())?; - validate_json(&json_doc, schema)?; - - for sign in &cose.signatures { - let key_id = &sign.protected.header.key_id; - anyhow::ensure!( - !key_id.is_empty(), - "COSE missing signature protected header `kid` field " - ); - - let kid = KidUri::try_from(key_id.as_ref())?; - println!("Signature Key ID: {kid}"); - let data_to_sign = cose.tbs_data(&[], sign); - let signature_bytes = sign.signature.as_slice().try_into().map_err(|_| { - anyhow::anyhow!( - "Invalid signature bytes size: expected {}, provided {}.", - ed25519_dalek::Signature::BYTE_SIZE, - sign.signature.len() - ) - })?; - println!( - "Verifying Key Len({}): 0x{}", - pk.as_bytes().len(), - hex::encode(pk.as_bytes()) - ); - let signature = ed25519_dalek::Signature::from_bytes(signature_bytes); - pk.verify_strict(&data_to_sign, &signature)?; - } - - Ok(()) -} - -fn validate_cose_protected_header(cose: &coset::CoseSign) -> anyhow::Result<()> { - 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" - ); - println!("HEADER REST: \n{:?}", cose.protected.header.rest); - 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" - ); - - // 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"); - // }; - // 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(()) -} diff --git a/rust/signed_doc/src/error.rs b/rust/signed_doc/src/error.rs index a33e4176b4c..a58ff104e34 100644 --- a/rust/signed_doc/src/error.rs +++ b/rust/signed_doc/src/error.rs @@ -2,18 +2,28 @@ /// Catalyst Signed Document error. #[derive(thiserror::Error, Debug)] -#[error("Catalyst Signed Document Error: {0:#?}")] -pub struct Error(pub(crate) Vec); +#[error("Catalyst Signed Document Error: {0:?}")] +pub struct Error(pub(crate) List); + +/// List of errors. +#[derive(Debug)] +pub(crate) struct List(pub(crate) Vec); + +impl From> for List { + fn from(e: Vec) -> Self { + Self(e) + } +} impl From> for Error { fn from(e: Vec) -> Self { - Error(e) + Self(e.into()) } } impl Error { /// List of errors. pub fn errors(&self) -> &Vec { - &self.0 + &self.0 .0 } } diff --git a/rust/signed_doc/src/lib.rs b/rust/signed_doc/src/lib.rs index d22d2b080e6..f93201aa648 100644 --- a/rust/signed_doc/src/lib.rs +++ b/rust/signed_doc/src/lib.rs @@ -107,14 +107,14 @@ impl Decode<'_, ()> for CatalystSignedDocument { let metadata = Metadata::try_from(&cose_sign.protected).map_or_else( |e| { - errors.extend(e.0); + errors.extend(e.0 .0); None }, Some, ); let signatures = Signatures::try_from(&cose_sign.signatures).map_or_else( |e| { - errors.extend(e.0); + errors.extend(e.0 .0); None }, Some, @@ -133,7 +133,7 @@ impl Decode<'_, ()> for CatalystSignedDocument { ) .map_err(|e| { errors.push(anyhow!("Invalid Document Content: {e}")); - minicbor::decode::Error::custom(error::Error(errors)) + minicbor::decode::Error::message(error::Error::from(errors)) })?; Ok(CatalystSignedDocument { @@ -145,7 +145,7 @@ impl Decode<'_, ()> for CatalystSignedDocument { .into(), }) }, - _ => Err(minicbor::decode::Error::custom(error::Error(errors))), + _ => Err(minicbor::decode::Error::message(error::Error::from(errors))), } } } diff --git a/rust/signed_doc/src/metadata/additional_fields.rs b/rust/signed_doc/src/metadata/additional_fields.rs index 9ea5b249245..384e74bf48f 100644 --- a/rust/signed_doc/src/metadata/additional_fields.rs +++ b/rust/signed_doc/src/metadata/additional_fields.rs @@ -269,7 +269,7 @@ impl TryFrom<&ProtectedHeader> for AdditionalFields { if errors.is_empty() { Ok(extra) } else { - Err(crate::error::Error(errors)) + Err(errors.into()) } } } diff --git a/rust/signed_doc/src/metadata/mod.rs b/rust/signed_doc/src/metadata/mod.rs index e4b951338c5..2a571972c06 100644 --- a/rust/signed_doc/src/metadata/mod.rs +++ b/rust/signed_doc/src/metadata/mod.rs @@ -172,7 +172,7 @@ impl TryFrom<&coset::ProtectedHeader> for Metadata { let extra = AdditionalFields::try_from(protected).map_or_else( |e| { - errors.extend(e.0); + errors.extend(e.0 .0); None }, Some, @@ -191,7 +191,7 @@ impl TryFrom<&coset::ProtectedHeader> for Metadata { errors.push(anyhow!( "Document Version {ver} cannot be smaller than Document ID {id}", )); - return Err(crate::error::Error(errors)); + return Err(crate::error::Error(errors.into())); } Ok(Self { @@ -203,7 +203,7 @@ impl TryFrom<&coset::ProtectedHeader> for Metadata { extra, }) }, - _ => Err(crate::error::Error(errors)), + _ => Err(crate::error::Error(errors.into())), } } } diff --git a/rust/signed_doc/src/signature/mod.rs b/rust/signed_doc/src/signature/mod.rs index e4add4333b7..37c23fab559 100644 --- a/rust/signed_doc/src/signature/mod.rs +++ b/rust/signed_doc/src/signature/mod.rs @@ -14,6 +14,7 @@ pub struct Signature { } /// List of Signatures. +#[derive(Debug)] pub struct Signatures(Vec); impl Signatures { @@ -45,9 +46,9 @@ impl TryFrom<&Vec> for Signatures { }); if errors.is_empty() { - Err(errors.into()) - } else { Ok(Signatures(signatures)) + } else { + Err(errors.into()) } } } From 8e5879e2848df72b07309326e626d09034d39dc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Rosales?= Date: Wed, 15 Jan 2025 13:53:18 -0600 Subject: [PATCH 67/71] fix(rust/signed_doc): more cleanup --- rust/signed_doc/examples/mk_signed_doc.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/rust/signed_doc/examples/mk_signed_doc.rs b/rust/signed_doc/examples/mk_signed_doc.rs index 2ee994efef1..b81170797e9 100644 --- a/rust/signed_doc/examples/mk_signed_doc.rs +++ b/rust/signed_doc/examples/mk_signed_doc.rs @@ -8,7 +8,7 @@ use std::{ path::PathBuf, }; -use catalyst_signed_doc::{CatalystSignedDocument, Decode, Decoder, DocumentRef, KidUri, Metadata}; +use catalyst_signed_doc::{CatalystSignedDocument, Decode, Decoder, KidUri, Metadata}; use clap::Parser; use coset::{iana::CoapContentFormat, CborSerializable}; use ed25519_dalek::{ed25519::signature::Signer, pkcs8::DecodePrivateKey}; @@ -65,11 +65,6 @@ fn encode_cbor_uuid(uuid: &uuid::Uuid) -> coset::cbor::Value { ) } -#[allow(clippy::indexing_slicing)] -fn _decode_cbor_document_ref(val: &coset::cbor::Value) -> anyhow::Result { - DocumentRef::try_from(val) -} - impl Cli { fn exec(self) -> anyhow::Result<()> { match self { From 00c3d811623c088a38909cab104d7225ca2bfaff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Rosales?= Date: Wed, 15 Jan 2025 13:56:44 -0600 Subject: [PATCH 68/71] fix(rust/signed-doc): fix spelling --- rust/signed_doc/examples/mk_signed_doc.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/signed_doc/examples/mk_signed_doc.rs b/rust/signed_doc/examples/mk_signed_doc.rs index b81170797e9..2bc3186c468 100644 --- a/rust/signed_doc/examples/mk_signed_doc.rs +++ b/rust/signed_doc/examples/mk_signed_doc.rs @@ -119,7 +119,7 @@ fn decode_signed_doc(cose_bytes: &[u8]) { println!("This is a valid Catalyst Signed Document."); println!("{cat_signed_doc}"); }, - Err(e) => eprintln!("Invalid Cataylyst Signed Document, err: {e}"), + Err(e) => eprintln!("Invalid Catalyst Signed Document, err: {e}"), } } From 886672d94b1bca194ed899966f517dd2977d6d50 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Thu, 16 Jan 2025 12:28:02 +0200 Subject: [PATCH 69/71] update Readme.md --- rust/signed_doc/README.md | 111 ++------------------------------------ 1 file changed, 5 insertions(+), 106 deletions(-) diff --git a/rust/signed_doc/README.md b/rust/signed_doc/README.md index fc9a80402d9..0fe4ef13c4b 100644 --- a/rust/signed_doc/README.md +++ b/rust/signed_doc/README.md @@ -2,90 +2,8 @@ # Catalyst signed document -Catalyst signed document is [COSE] based document structure, -particularly `COSE Signed Data Object` [COSE] type. - -## Structure - -This document structure is fully inherits an original [COSE] design and specifies the details -of different [COSE] header's fields. - -### Protected header - -The [COSE] standard defines two types of headers: `protected` and `unprotected`. -Catalyst signed document specifies the following `protected` header fields, -which **must** be present (most of the fields originally defined by this -[spec](https://input-output-hk.github.io/catalyst-voices/architecture/08_concepts/signed_document_metadata/metadata/)): - -* `alg`: `EdDSA` - (this parameter is used to indicate the algorithm used for the security processing, - in this particular case `ed25119` signature algorithm is used). -* `content type`: `application/json` - (this parameter is used to indicate the content type of the payload data, - in this particular case `JSON` format is used). -* `content encoding` (CBOR type `text`): `br` CBOR type `text` - (this parameter is used to indicate the content encodings algorithm of the payload data, - in this particular case [brotli] compression data format is used). -* `type`: CBOR encoded UUID. -* `id`: CBOR encoded UUID. -* `ver`: CBOR encoded UUID. -* `ref`: CBOR encoded UUID or two elements array of UUID (optional). -* `template`: CBOR encoded UUID or two elements array of UUID (optional). -* `reply`: CBOR encoded UUID or two elements array of UUID (optional). -* `section`: CBOR encoded string (optional). -* `collabs`: CBOR encoded array of any CBOR types (optional). - -Precise CDDL definition - -```cddl -; All encoders/decoders of this specification must follow deterministic cbor encoding rules -; https://datatracker.ietf.org/doc/html/draft-ietf-cbor-cde-06 - -protected_header = { - 1 => -8, ; "alg": EdDSA - 3 => 30, ; "content type": Json - "content encoding" => "br", ; payload content encoding, brotli compression - "type" => UUID, - "id" => UUID, - "ver" => UUID, - ? "ref" => reference_type, - ? "template" => reference_type, - ? "reply" => reference_type, - ? "section" => text, - ? "collabs" => [+any], -} - -UUID = #6.37(bytes) -reference_type = UUID / [UUID, UUID] ; either UUID or [UUID, UUID] -``` - -### COSE payload - -The [COSE] signature payload, as mentioned earlier, -the content type of the [COSE] signature payload is JSON, [brotli] compressed. -Which stores an actual document data which should follow to some schema. - -### Signature protected header - -As it mentioned earlier, Catalyst signed document utilizes `COSE Signed Data Object` format, -which allows to provide multi-signature functionality. -In that regard, -each Catalyst signed document [COSE] signature **must** include the following protected header field: - -`protected`: - -* `kid`: CBOR encoded `bytes` type. - -Precise CDDL definition - -```cddl -; All encoders/decoders of this specification must follow deterministic cbor encoding rules -; https://datatracker.ietf.org/doc/html/draft-ietf-cbor-cde-06 - -signature_protected_header = { - 4 => bytes ; "kid" -} -``` +Catalyst signed document crate implementation based on this +[spec](https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/signed_doc/spec/) ## Example @@ -99,33 +17,14 @@ Prepare non-signed document, `meta.json` file should follow the [`meta.schema.json`](./meta.schema.json). ```shell -cargo run -p signed_doc --example mk_signed_doc build +cargo run -p catalyst-signed-doc --example mk_signed_doc build signed_doc/doc.json signed_doc/schema.json signed_doc/doc.cose signed_doc/meta.json ``` -Sign document +Inspect document ```shell -cargo run -p signed_doc --example mk_signed_doc sign private.pem signed_doc/doc.cose kid_1 -``` - -Verify document - -```shell -cargo run -p signed_doc --example mk_signed_doc verify -public.pem signed_doc/doc.cose signed_doc/schema.json -``` - -Catalyst signed document CBOR bytes example - -```cbor -845861A6012703183270636F6E74656E7420656E636F64696E676262726474797065D825500CE8AB3892584FBCA62E7F -AA6E58318F626964D9800C500193929C1D227F1977FED19443841F0B63766572D9800C500193929C1D227F1977FED194 -43841F0BA0584E1B6D00209C05762C9B4E1EAC3DCA9286B50888CBDE8E99A2EB532C3A0D83D6F6462707ECDFF7F9B74B -8904098479CA4221337F7DB97FDA25AFCC10ECB75722C91A485AAC1158BA6F90619221066C828347A104446B696431A0 -584090DF51433D97728ACF3851C5D3CA2908F76589EA925AF434C5619234E4B1BA7B12A124EA79503562B33214EBC730 -C9837E1CA909BB8163D7904B09C3FD6A5B0B8347A104446B696432A05840AB318FEF3FF46E69E760540B0B44E9E8A51A -84F23EC8A870ECDEBF9AD98EBB8212EBE5EA5FDBA87C98DF8DF259BE7873FE8B9EB54CC6558337B5C95D90CC3504 +cargo run -p catalyst-signed-doc --example mk_signed_doc inspect signed_doc/doc.cose ``` [COSE]: https://datatracker.ietf.org/doc/html/rfc9052 From cd817a4cceabd767a18dfc6f6ff799a57f93ad93 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Thu, 16 Jan 2025 12:31:36 +0200 Subject: [PATCH 70/71] fix README.md --- rust/signed_doc/README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/rust/signed_doc/README.md b/rust/signed_doc/README.md index 0fe4ef13c4b..53d691be9f6 100644 --- a/rust/signed_doc/README.md +++ b/rust/signed_doc/README.md @@ -26,6 +26,3 @@ Inspect document ```shell cargo run -p catalyst-signed-doc --example mk_signed_doc inspect signed_doc/doc.cose ``` - -[COSE]: https://datatracker.ietf.org/doc/html/rfc9052 -[brotli]: https://datatracker.ietf.org/doc/html/rfc7932 From 2f0c18680dacb8e3a9a1d159a08efe34893be85e Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Thu, 16 Jan 2025 12:43:44 +0200 Subject: [PATCH 71/71] fix earthfile --- rust/Earthfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/Earthfile b/rust/Earthfile index 71a68d8fb3c..230941a0f76 100644 --- a/rust/Earthfile +++ b/rust/Earthfile @@ -58,7 +58,7 @@ build: --args1="--libs=c509-certificate --libs=cardano-blockchain-types --libs=cardano-chain-follower --libs=hermes-ipfs" \ --args2="--libs=cbork-cddl-parser --libs=cbork-abnf-parser --libs=cbork-utils --libs=catalyst-types" \ --args3="--libs=catalyst-voting --libs=immutable-ledger --libs=vote-tx-v1 --libs=vote-tx-v2" \ - --args4="--bins=cbork/cbork --libs=rbac-registration --libs=signed_doc" \ + --args4="--bins=cbork/cbork --libs=rbac-registration --libs=catalyst-signed-doc" \ --args5="--cov_report=$HOME/build/coverage-report.info" \ --output="release/[^\./]+" \ --junit="cat-libs.junit-report.xml" \