diff --git a/rust/catalyst-signed-doc-spec/Cargo.toml b/rust/catalyst-signed-doc-spec/Cargo.toml index 336f0ec13a6..8fed46f3462 100644 --- a/rust/catalyst-signed-doc-spec/Cargo.toml +++ b/rust/catalyst-signed-doc-spec/Cargo.toml @@ -11,6 +11,9 @@ license.workspace = true workspace = true [dependencies] +catalyst-types = { version = "0.0.11", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "catalyst-types/v0.0.11" } +cbork-cddl-parser = { version = "0.0.3", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "cbork-cddl-parser/v0.0.3" } + serde_json = "1.0.142" anyhow = "1.0.99" serde = { version = "1.0.219", features = ["derive"] } diff --git a/rust/catalyst-signed-doc-spec/src/cddl_definitions.rs b/rust/catalyst-signed-doc-spec/src/cddl_definitions.rs new file mode 100644 index 00000000000..5f5249308f5 --- /dev/null +++ b/rust/catalyst-signed-doc-spec/src/cddl_definitions.rs @@ -0,0 +1,79 @@ +//! 'cddlDefinitions' field definition + +use std::{collections::HashMap, fmt::Display}; + +use cbork_cddl_parser::validate_cddl; + +#[derive(serde::Deserialize)] +pub struct CddlDefinitions(HashMap); + +#[derive(Clone, serde::Deserialize, PartialEq, Eq, Hash)] +pub struct CddlType(String); + +#[derive(serde::Deserialize)] +struct CddlDef { + def: String, + requires: Vec, +} + +impl Display for CddlType { + fn fmt( + &self, + f: &mut std::fmt::Formatter<'_>, + ) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl CddlDef { + fn get_cddl_spec( + &self, + cddl_type: &CddlType, + ) -> String { + format!("{cddl_type}={}\n", self.def) + } +} + +impl CddlDefinitions { + fn find_cddl_def( + &self, + cddl_type: &CddlType, + ) -> anyhow::Result<&CddlDef> { + self.0.get(cddl_type).ok_or(anyhow::anyhow!( + "Cannot find a cddl definition for the {cddl_type}" + )) + } + + /// Returns a full CDDL specification schema. + /// Performs + /// + /// # Errors + /// - Cannot find a cddl definition + /// - Not a valid resulted CDDL spec + pub fn get_cddl_spec( + &self, + cddl_type: &CddlType, + ) -> anyhow::Result { + let def = self.find_cddl_def(cddl_type)?; + + let spec = def.get_cddl_spec(cddl_type); + let mut requires = def.requires.clone(); + + let mut imports = HashMap::new(); + while let Some(req) = requires.pop() { + let req_def = self.find_cddl_def(&req)?; + let req_spec = req_def.get_cddl_spec(&req); + if imports.insert(req, req_spec).is_none() { + requires.extend(req_def.requires.clone()); + } + } + + let mut spec = imports.values().fold(spec, |mut spec, import_spec| { + spec.push_str(import_spec); + spec + }); + + validate_cddl(&mut spec, &cbork_cddl_parser::Extension::CDDL)?; + Ok(spec) + } +} diff --git a/rust/catalyst-signed-doc-spec/src/lib.rs b/rust/catalyst-signed-doc-spec/src/lib.rs index e1d19f62118..16bf1f8903c 100644 --- a/rust/catalyst-signed-doc-spec/src/lib.rs +++ b/rust/catalyst-signed-doc-spec/src/lib.rs @@ -2,7 +2,8 @@ #![allow(missing_docs, clippy::missing_docs_in_private_items)] -pub mod copyright; +pub mod cddl_definitions; +mod copyright; pub mod doc_types; pub mod headers; pub mod is_required; @@ -15,7 +16,8 @@ use std::{collections::HashMap, fmt::Display, ops::Deref}; use build_info as build_info_lib; use crate::{ - copyright::Copyright, headers::Headers, metadata::Metadata, payload::Payload, signers::Signers, + cddl_definitions::CddlDefinitions, copyright::Copyright, headers::Headers, metadata::Metadata, + payload::Payload, signers::Signers, }; build_info_lib::build_info!(pub(crate) fn build_info); @@ -23,8 +25,10 @@ build_info_lib::build_info!(pub(crate) fn build_info); /// Catalyst Signed Document spec representation struct #[derive(serde::Deserialize)] pub struct CatalystSignedDocSpec { - pub docs: DocSpecs, + #[serde(rename = "cddlDefinitions")] + pub cddl_definitions: CddlDefinitions, copyright: Copyright, + pub docs: DocSpecs, } #[derive(serde::Deserialize)] diff --git a/rust/catalyst-signed-doc-spec/src/payload.rs b/rust/catalyst-signed-doc-spec/src/payload.rs index bcf18e63415..bbd4da07183 100644 --- a/rust/catalyst-signed-doc-spec/src/payload.rs +++ b/rust/catalyst-signed-doc-spec/src/payload.rs @@ -1,9 +1,40 @@ //! `signed_doc.json` "payload" field JSON definition +use catalyst_types::json_schema::JsonSchema; +use serde::Deserialize; + +use crate::cddl_definitions::CddlType; + /// `signed_doc.json` "payload" field JSON object #[derive(serde::Deserialize)] #[allow(clippy::missing_docs_in_private_items)] pub struct Payload { pub nil: bool, - pub schema: Option, + pub schema: Option, +} + +pub enum Schema { + Cddl(CddlType), + Json(JsonSchema), +} + +impl<'de> Deserialize<'de> for Schema { + fn deserialize(deserializer: D) -> Result + where D: serde::Deserializer<'de> { + #[derive(serde::Deserialize)] + #[serde(untagged)] + pub enum SchemaSerde { + Cddl(CddlType), + Json(serde_json::Value), + } + + match SchemaSerde::deserialize(deserializer)? { + SchemaSerde::Json(json) => { + JsonSchema::try_from(&json) + .map(Self::Json) + .map_err(serde::de::Error::custom) + }, + SchemaSerde::Cddl(cddl_type) => Ok(Self::Cddl(cddl_type)), + } + } } diff --git a/rust/signed_doc/Cargo.toml b/rust/signed_doc/Cargo.toml index 4af954baef3..3a0e3f3b82b 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.10", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "catalyst-types/v0.0.10" } +catalyst-types = { version = "0.0.11", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "catalyst-types/v0.0.11" } cbork-utils = { version = "0.0.2", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "cbork-utils-v0.0.2" } catalyst-signed-doc-macro = { version = "0.0.1", path = "../catalyst-signed-doc-macro" } diff --git a/rust/signed_doc/src/validator/json_schema.rs b/rust/signed_doc/src/validator/json_schema.rs deleted file mode 100644 index 1ef2ef391cb..00000000000 --- a/rust/signed_doc/src/validator/json_schema.rs +++ /dev/null @@ -1,139 +0,0 @@ -//! A wrapper around a JSON Schema validator. - -use std::ops::Deref; - -use anyhow::anyhow; -use jsonschema::{options, Draft, Validator}; -use serde_json::Value; - -/// Wrapper around a JSON Schema validator. -/// -/// Attempts to detect the draft version from the `$schema` field. -/// If not specified, it tries Draft2020-12 first, then falls back to Draft7. -/// Returns an error if schema is invalid for both. -pub(crate) struct JsonSchema(Validator); - -impl Deref for JsonSchema { - type Target = Validator; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl TryFrom<&Value> for JsonSchema { - type Error = anyhow::Error; - - fn try_from(schema: &Value) -> std::result::Result { - let draft_version = if let Some(schema) = schema.get("$schema").and_then(|s| s.as_str()) { - if schema.contains("draft-07") { - Some(Draft::Draft7) - } else if schema.contains("2020-12") { - Some(Draft::Draft202012) - } else { - None - } - } else { - None - }; - - if let Some(draft) = draft_version { - let validator = options() - .with_draft(draft) - .build(schema) - .map_err(|e| anyhow!("Invalid JSON Schema: {e}"))?; - - Ok(JsonSchema(validator)) - } else { - // if draft not specified or not detectable: - // try draft2020-12 - if let Ok(validator) = options().with_draft(Draft::Draft202012).build(schema) { - return Ok(JsonSchema(validator)); - } - - // fallback to draft7 - if let Ok(validator) = options().with_draft(Draft::Draft7).build(schema) { - return Ok(JsonSchema(validator)); - } - - Err(anyhow!( - "Could not detect draft version and schema is not valid against Draft2020-12 or Draft7" - )) - } - } -} - -#[cfg(test)] -mod tests { - use serde_json::json; - - use super::*; - - #[test] - fn valid_draft7_schema() { - let schema = json!({ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "name": { "type": "string" } - } - }); - - let result = JsonSchema::try_from(&schema); - assert!(result.is_ok(), "Expected Draft7 schema to be valid"); - } - - #[test] - fn valid_draft2020_12_schema() { - let schema = json!({ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", - "properties": { - "age": { "type": "integer" } - } - }); - - let result = JsonSchema::try_from(&schema); - assert!(result.is_ok(), "Expected Draft2020-12 schema to be valid"); - } - - #[test] - fn schema_without_draft_should_fallback() { - // Valid in both Draft2020-12 and Draft7 - let schema = json!({ - "type": "object", - "properties": { - "id": { "type": "number" } - } - }); - - let result = JsonSchema::try_from(&schema); - assert!( - result.is_ok(), - "Expected schema without $schema to fallback and succeed" - ); - } - - #[test] - fn invalid_schema_should_error() { - // Invalid schema: "type" is not a valid keyword here - let schema = json!({ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "not-a-valid-type" - }); - - let result = JsonSchema::try_from(&schema); - assert!( - result.is_err(), - "Expected invalid schema to return an error" - ); - } - - #[test] - fn empty_object_schema() { - let schema = json!({}); - - let result = JsonSchema::try_from(&schema); - assert!(result.is_ok()); - } -} diff --git a/rust/signed_doc/src/validator/mod.rs b/rust/signed_doc/src/validator/mod.rs index 7758b4d906b..f9e1f16f3f0 100644 --- a/rust/signed_doc/src/validator/mod.rs +++ b/rust/signed_doc/src/validator/mod.rs @@ -1,6 +1,5 @@ //! Catalyst Signed Documents validation logic -pub(crate) mod json_schema; pub(crate) mod rules; use std::{collections::HashMap, sync::LazyLock}; diff --git a/rust/signed_doc/src/validator/rules/content/mod.rs b/rust/signed_doc/src/validator/rules/content/mod.rs index 46cb2b4c491..7895270fcd8 100644 --- a/rust/signed_doc/src/validator/rules/content/mod.rs +++ b/rust/signed_doc/src/validator/rules/content/mod.rs @@ -5,19 +5,22 @@ mod tests; use std::fmt::Debug; -use catalyst_signed_doc_spec::payload::Payload; +use catalyst_signed_doc_spec::{ + cddl_definitions::CddlDefinitions, + payload::{Payload, Schema}, +}; +use catalyst_types::json_schema::JsonSchema; use minicbor::Encode; -use crate::{ - validator::{json_schema, rules::utils::content_json_schema_check}, - CatalystSignedDocument, -}; +use crate::{validator::rules::utils::content_json_schema_check, CatalystSignedDocument}; /// Enum represents different content schemas, against which documents content would be /// validated. pub(crate) enum ContentSchema { /// Draft 7 JSON schema - Json(json_schema::JsonSchema), + Json(JsonSchema), + /// CDDL schema + Cddl, } impl Debug for ContentSchema { @@ -27,6 +30,7 @@ impl Debug for ContentSchema { ) -> std::fmt::Result { match self { Self::Json(_) => writeln!(f, "JsonSchema"), + Self::Cddl => writeln!(f, "CddlSchema"), } } } @@ -44,7 +48,10 @@ pub(crate) enum ContentRule { impl ContentRule { /// Generating `ContentRule` from specs - pub(crate) fn new(spec: &Payload) -> anyhow::Result { + pub(crate) fn new( + cddl_def: &CddlDefinitions, + spec: &Payload, + ) -> anyhow::Result { if spec.nil { anyhow::ensure!( spec.schema.is_none(), @@ -53,13 +60,16 @@ impl ContentRule { return Ok(Self::Nil); } - if let Some(schema) = &spec.schema { - let schema_str = schema.to_string(); - Ok(Self::StaticSchema(ContentSchema::Json( - json_schema::JsonSchema::try_from(&serde_json::from_str(&schema_str)?)?, - ))) - } else { - Ok(Self::NotNil) + match &spec.schema { + Some(Schema::Json(schema)) => { + Ok(Self::StaticSchema(ContentSchema::Json(schema.clone()))) + }, + Some(Schema::Cddl(cddl_type)) => { + cddl_def + .get_cddl_spec(cddl_type) + .map(|_| Self::StaticSchema(ContentSchema::Cddl)) + }, + None => Ok(Self::NotNil), } } @@ -75,6 +85,7 @@ impl ContentRule { ContentSchema::Json(json_schema) => { return Ok(content_json_schema_check(doc, json_schema)) }, + ContentSchema::Cddl => return Ok(true), } } if let Self::NotNil = self { diff --git a/rust/signed_doc/src/validator/rules/content/tests.rs b/rust/signed_doc/src/validator/rules/content/tests.rs index b8fb3394df8..0fac1b56df4 100644 --- a/rust/signed_doc/src/validator/rules/content/tests.rs +++ b/rust/signed_doc/src/validator/rules/content/tests.rs @@ -1,3 +1,4 @@ +use catalyst_types::json_schema::JsonSchema; use test_case::test_case; use super::*; @@ -36,7 +37,7 @@ use crate::builder::tests::Builder; async fn content_rule_specified_test( doc_gen: impl FnOnce(Vec) -> CatalystSignedDocument ) -> bool { - let schema = json_schema::JsonSchema::try_from(&serde_json::json!({})).unwrap(); + let schema = JsonSchema::try_from(&serde_json::json!({})).unwrap(); let content_schema = ContentSchema::Json(schema); let valid_content = serde_json::to_vec(&serde_json::json!({})).unwrap(); diff --git a/rust/signed_doc/src/validator/rules/content_type/mod.rs b/rust/signed_doc/src/validator/rules/content_type/mod.rs index f9933da9981..a85404e9219 100644 --- a/rust/signed_doc/src/validator/rules/content_type/mod.rs +++ b/rust/signed_doc/src/validator/rules/content_type/mod.rs @@ -3,7 +3,9 @@ #[cfg(test)] mod tests; -use crate::{metadata::ContentType, validator::json_schema::JsonSchema, CatalystSignedDocument}; +use catalyst_types::json_schema::JsonSchema; + +use crate::{metadata::ContentType, CatalystSignedDocument}; /// `content-type` field validation rule #[derive(Debug)] diff --git a/rust/signed_doc/src/validator/rules/mod.rs b/rust/signed_doc/src/validator/rules/mod.rs index 112c28be208..f1ca23fab5f 100644 --- a/rust/signed_doc/src/validator/rules/mod.rs +++ b/rust/signed_doc/src/validator/rules/mod.rs @@ -2,7 +2,7 @@ //! use anyhow::Context; -use catalyst_signed_doc_spec::{DocSpec, DocSpecs}; +use catalyst_signed_doc_spec::{cddl_definitions::CddlDefinitions, DocSpec, DocSpecs}; use futures::FutureExt; use crate::{ @@ -118,6 +118,7 @@ impl Rules { /// Creating a `Rules` instance from the provided specs. fn new( + cddl_defs: &CddlDefinitions, all_docs_specs: &DocSpecs, doc_spec: &DocSpec, ) -> anyhow::Result { @@ -133,7 +134,7 @@ impl Rules { reply: ReplyRule::new(all_docs_specs, &doc_spec.metadata.reply)?, section: SectionRule::NotSpecified, collaborators: CollaboratorsRule::new(&doc_spec.metadata.collaborators), - content: ContentRule::new(&doc_spec.payload)?, + content: ContentRule::new(cddl_defs, &doc_spec.payload)?, kid: SignatureKidRule::new(&doc_spec.signers.roles)?, signature: SignatureRule, ownership: DocumentOwnershipRule::new(&doc_spec.signers.update, doc_spec)?, @@ -157,7 +158,7 @@ impl Rules { continue; } - let rules = Self::new(&spec.docs, doc_spec) + let rules = Self::new(&spec.cddl_definitions, &spec.docs, doc_spec) .context(format!("Fail to initializing document '{doc_name}'"))?; let doc_type = doc_spec.doc_type.parse()?; diff --git a/rust/signed_doc/src/validator/rules/template/mod.rs b/rust/signed_doc/src/validator/rules/template/mod.rs index c1214e3c418..72bfd8f6adb 100644 --- a/rust/signed_doc/src/validator/rules/template/mod.rs +++ b/rust/signed_doc/src/validator/rules/template/mod.rs @@ -6,13 +6,11 @@ mod tests; use catalyst_signed_doc_spec::{ is_required::IsRequired, metadata::template::Template, DocSpecs, DocumentName, }; +use catalyst_types::json_schema::JsonSchema; use crate::{ providers::CatalystSignedDocumentProvider, - validator::{ - json_schema::JsonSchema, - rules::{doc_ref::doc_refs_check, utils::content_json_schema_check}, - }, + validator::rules::{doc_ref::doc_refs_check, utils::content_json_schema_check}, CatalystSignedDocument, ContentType, DocType, }; diff --git a/rust/signed_doc/src/validator/rules/utils.rs b/rust/signed_doc/src/validator/rules/utils.rs index 58c8e84ec90..cd2eee42f51 100644 --- a/rust/signed_doc/src/validator/rules/utils.rs +++ b/rust/signed_doc/src/validator/rules/utils.rs @@ -2,7 +2,9 @@ use std::fmt::Write; -use crate::{validator::json_schema::JsonSchema, CatalystSignedDocument}; +use catalyst_types::json_schema::JsonSchema; + +use crate::CatalystSignedDocument; /// Validating the document's content against the provided JSON schema pub(crate) fn content_json_schema_check(