diff --git a/.config/dictionaries/project.dic b/.config/dictionaries/project.dic index 8fe738a180..61470dcae1 100644 --- a/.config/dictionaries/project.dic +++ b/.config/dictionaries/project.dic @@ -192,6 +192,7 @@ MPMC msvc Multiaddr multiera +mutlisig mypy nanos netkey diff --git a/rust/signed_doc/src/providers.rs b/rust/signed_doc/src/providers.rs index e99b2eddd1..968fdad858 100644 --- a/rust/signed_doc/src/providers.rs +++ b/rust/signed_doc/src/providers.rs @@ -8,12 +8,12 @@ use ed25519_dalek::VerifyingKey; use crate::{CatalystSignedDocument, DocumentRef}; /// `VerifyingKey` Provider trait -pub trait VerifyingKeyProvider { +pub trait VerifyingKeyProvider: Send + Sync { /// Try to get `VerifyingKey` fn try_get_key( &self, kid: &CatalystId, - ) -> impl Future>>; + ) -> impl Future>> + Send; } /// `CatalystSignedDocument` Provider trait @@ -53,11 +53,16 @@ pub mod tests { }; use crate::{DocLocator, DocumentRef}; - /// Simple testing implementation of `CatalystSignedDocumentProvider` + /// Simple testing implementation of `CatalystSignedDocumentProvider`, #[derive(Default, Debug)] - pub struct TestCatalystSignedDocumentProvider(HashMap); + pub struct TestCatalystProvider { + /// For `CatalystSignedDocumentProvider`. + signed_doc: HashMap, + /// For `VerifyingKeyProvider`. + verifying_key: HashMap, + } - impl TestCatalystSignedDocumentProvider { + impl TestCatalystProvider { /// Inserts document into the `TestCatalystSignedDocumentProvider` where /// if document reference is provided use that value. /// if not use the id and version of the provided doc. @@ -71,21 +76,30 @@ pub mod tests { doc: &CatalystSignedDocument, ) -> anyhow::Result<()> { if let Some(dr) = doc_ref { - self.0.insert(dr, doc.clone()); + self.signed_doc.insert(dr, doc.clone()); } else { let dr = DocumentRef::new(doc.doc_id()?, doc.doc_ver()?, DocLocator::default()); - self.0.insert(dr, doc.clone()); + self.signed_doc.insert(dr, doc.clone()); } Ok(()) } + + /// Inserts public key into the `TestVerifyingKeyProvider` + pub fn add_pk( + &mut self, + kid: CatalystId, + pk: VerifyingKey, + ) { + self.verifying_key.insert(kid, pk); + } } - impl CatalystSignedDocumentProvider for TestCatalystSignedDocumentProvider { + impl CatalystSignedDocumentProvider for TestCatalystProvider { async fn try_get_doc( &self, doc_ref: &DocumentRef, ) -> anyhow::Result> { - Ok(self.0.get(doc_ref).cloned()) + Ok(self.signed_doc.get(doc_ref).cloned()) } async fn try_get_last_doc( @@ -93,7 +107,7 @@ pub mod tests { id: catalyst_types::uuid::UuidV7, ) -> anyhow::Result> { Ok(self - .0 + .signed_doc .iter() .filter(|(doc_ref, _)| doc_ref.id() == &id) .max_by_key(|(doc_ref, _)| doc_ref.ver().uuid()) @@ -109,27 +123,12 @@ pub mod tests { } } - /// Simple testing implementation of `VerifyingKeyProvider` - #[derive(Default)] - pub struct TestVerifyingKeyProvider(HashMap); - - impl TestVerifyingKeyProvider { - /// Inserts public key into the `TestVerifyingKeyProvider` - pub fn add_pk( - &mut self, - kid: CatalystId, - pk: VerifyingKey, - ) { - self.0.insert(kid, pk); - } - } - - impl VerifyingKeyProvider for TestVerifyingKeyProvider { + impl VerifyingKeyProvider for TestCatalystProvider { async fn try_get_key( &self, kid: &CatalystId, ) -> anyhow::Result> { - Ok(self.0.get(kid).copied()) + Ok(self.verifying_key.get(kid).copied()) } } } diff --git a/rust/signed_doc/src/validator/mod.rs b/rust/signed_doc/src/validator/mod.rs index 4d370da1c9..09db8a1263 100644 --- a/rust/signed_doc/src/validator/mod.rs +++ b/rust/signed_doc/src/validator/mod.rs @@ -8,8 +8,7 @@ use std::{ sync::{Arc, LazyLock}, }; -use anyhow::Context; -use catalyst_types::{catalyst_id::role_index::RoleId, problem_report::ProblemReport}; +use catalyst_types::catalyst_id::role_index::RoleId; use rules::{ ContentEncodingRule, ContentRule, ContentSchema, ContentTypeRule, IdRule, ParametersRule, RefRule, ReplyRule, Rules, SectionRule, SignatureKidRule, VerRule, @@ -22,7 +21,7 @@ use crate::{ }, metadata::DocType, providers::{CatalystSignedDocumentProvider, VerifyingKeyProvider}, - signature::{tbs_data, Signature}, + validator::rules::SignatureRule, CatalystSignedDocument, ContentEncoding, ContentType, }; @@ -62,6 +61,7 @@ fn proposal_rule() -> Rules { kid: SignatureKidRule { exp: &[RoleId::Proposer], }, + signature: SignatureRule { mutlisig: false }, } } @@ -104,6 +104,7 @@ fn proposal_comment_rule() -> Rules { kid: SignatureKidRule { exp: &[RoleId::Role0], }, + signature: SignatureRule { mutlisig: false }, } } @@ -152,6 +153,7 @@ fn proposal_submission_action_rule() -> Rules { kid: SignatureKidRule { exp: &[RoleId::Proposer], }, + signature: SignatureRule { mutlisig: false }, } } @@ -185,7 +187,7 @@ pub async fn validate( provider: &Provider, ) -> anyhow::Result where - Provider: CatalystSignedDocumentProvider, + Provider: CatalystSignedDocumentProvider + VerifyingKeyProvider, { let Ok(doc_type) = doc.doc_type() else { doc.report().missing_field( @@ -207,82 +209,6 @@ where rules.check(doc, provider).await } -/// Verify document signatures. -/// Return true if all signatures are valid, otherwise return false. -/// -/// # Errors -/// If `provider` returns error, fails fast throwing that error. -pub async fn validate_signatures( - doc: &CatalystSignedDocument, - provider: &impl VerifyingKeyProvider, -) -> anyhow::Result { - if doc.signatures().is_empty() { - doc.report().other( - "Catalyst Signed Document is unsigned", - "During Catalyst Signed Document signature validation", - ); - return Ok(false); - } - - let sign_rules = doc - .signatures() - .iter() - .map(|sign| validate_signature(doc, sign, provider, doc.report())); - - let res = futures::future::join_all(sign_rules) - .await - .into_iter() - .collect::>>()? - .iter() - .all(|res| *res); - - Ok(res) -} - -/// A single signature validation function -async fn validate_signature( - doc: &CatalystSignedDocument, - sign: &Signature, - provider: &Provider, - report: &ProblemReport, -) -> anyhow::Result -where - Provider: VerifyingKeyProvider, -{ - let kid = sign.kid(); - - let Some(pk) = provider.try_get_key(kid).await? else { - report.other( - &format!("Missing public key for {kid}."), - "During public key extraction", - ); - return Ok(false); - }; - - let tbs_data = tbs_data(kid, doc.doc_meta(), doc.content()).context("Probably a bug, cannot build CBOR COSE bytes for signature verification from the structurally valid COSE object.")?; - - let Ok(signature_bytes) = sign.signature().try_into() else { - report.invalid_value( - "cose signature", - &format!("{}", sign.signature().len()), - &format!("must be {}", ed25519_dalek::Signature::BYTE_SIZE), - "During encoding cose signature to bytes", - ); - return Ok(false); - }; - - let signature = ed25519_dalek::Signature::from_bytes(signature_bytes); - if pk.verify_strict(&tbs_data, &signature).is_err() { - report.functional_validation( - &format!("Verification failed for signature with Key ID {kid}"), - "During signature validation with verifying key", - ); - return Ok(false); - } - - Ok(true) -} - #[cfg(test)] mod tests { use crate::validator::document_rules_init; diff --git a/rust/signed_doc/src/validator/rules/doc_ref.rs b/rust/signed_doc/src/validator/rules/doc_ref.rs index eccb1d0a45..d5bd1ae5dd 100644 --- a/rust/signed_doc/src/validator/rules/doc_ref.rs +++ b/rust/signed_doc/src/validator/rules/doc_ref.rs @@ -188,8 +188,8 @@ mod tests { use super::*; use crate::{ - builder::tests::Builder, metadata::SupportedField, - providers::tests::TestCatalystSignedDocumentProvider, DocLocator, DocumentRef, + builder::tests::Builder, metadata::SupportedField, providers::tests::TestCatalystProvider, + DocLocator, DocumentRef, }; #[test_case( @@ -397,12 +397,9 @@ mod tests { )] #[tokio::test] async fn ref_specified_test( - doc_gen: impl FnOnce( - &[DocType; 2], - &mut TestCatalystSignedDocumentProvider, - ) -> CatalystSignedDocument + doc_gen: impl FnOnce(&[DocType; 2], &mut TestCatalystProvider) -> CatalystSignedDocument ) -> bool { - let mut provider = TestCatalystSignedDocumentProvider::default(); + let mut provider = TestCatalystProvider::default(); let exp_types: [DocType; 2] = [UuidV4::new().into(), UuidV4::new().into()]; @@ -430,7 +427,7 @@ mod tests { #[tokio::test] async fn ref_specified_optional_test() { - let provider = TestCatalystSignedDocumentProvider::default(); + let provider = TestCatalystProvider::default(); let rule = RefRule::Specified { exp_ref_types: vec![UuidV4::new().into()], optional: true, @@ -439,7 +436,7 @@ mod tests { let doc = Builder::new().build(); assert!(rule.check(&doc, &provider).await.unwrap()); - let provider = TestCatalystSignedDocumentProvider::default(); + let provider = TestCatalystProvider::default(); let rule = RefRule::Specified { exp_ref_types: vec![UuidV4::new().into()], optional: false, @@ -452,7 +449,7 @@ mod tests { #[tokio::test] async fn ref_rule_not_specified_test() { let rule = RefRule::NotSpecified; - let provider = TestCatalystSignedDocumentProvider::default(); + let provider = TestCatalystProvider::default(); let doc = Builder::new().build(); assert!(rule.check(&doc, &provider).await.unwrap()); diff --git a/rust/signed_doc/src/validator/rules/id.rs b/rust/signed_doc/src/validator/rules/id.rs index 0d694e2b94..a9cc03d4d9 100644 --- a/rust/signed_doc/src/validator/rules/id.rs +++ b/rust/signed_doc/src/validator/rules/id.rs @@ -101,8 +101,8 @@ mod tests { use super::*; use crate::{ - builder::tests::Builder, metadata::SupportedField, - providers::tests::TestCatalystSignedDocumentProvider, UuidV7, + builder::tests::Builder, metadata::SupportedField, providers::tests::TestCatalystProvider, + UuidV7, }; #[test_case( @@ -170,9 +170,9 @@ mod tests { )] #[tokio::test] async fn id_test( - doc_gen: impl FnOnce(&TestCatalystSignedDocumentProvider) -> CatalystSignedDocument + doc_gen: impl FnOnce(&TestCatalystProvider) -> CatalystSignedDocument ) -> bool { - let provider = TestCatalystSignedDocumentProvider::default(); + let provider = TestCatalystProvider::default(); let doc = doc_gen(&provider); IdRule.check(&doc, &provider).await.unwrap() diff --git a/rust/signed_doc/src/validator/rules/mod.rs b/rust/signed_doc/src/validator/rules/mod.rs index be934089ab..7554325786 100644 --- a/rust/signed_doc/src/validator/rules/mod.rs +++ b/rust/signed_doc/src/validator/rules/mod.rs @@ -3,7 +3,10 @@ use futures::FutureExt; -use crate::{providers::CatalystSignedDocumentProvider, CatalystSignedDocument}; +use crate::{ + providers::{CatalystSignedDocumentProvider, VerifyingKeyProvider}, + CatalystSignedDocument, +}; mod content_encoding; mod content_type; @@ -12,6 +15,7 @@ mod id; mod parameters; mod reply; mod section; +mod signature; mod signature_kid; mod template; mod ver; @@ -23,6 +27,7 @@ pub(crate) use id::IdRule; pub(crate) use parameters::ParametersRule; pub(crate) use reply::ReplyRule; pub(crate) use section::SectionRule; +pub(crate) use signature::SignatureRule; pub(crate) use signature_kid::SignatureKidRule; pub(crate) use template::{ContentRule, ContentSchema}; pub(crate) use ver::VerRule; @@ -49,6 +54,8 @@ pub(crate) struct Rules { pub(crate) parameters: ParametersRule, /// `kid` field validation rule pub(crate) kid: SignatureKidRule, + /// document's signatures validation rule + pub(crate) signature: SignatureRule, } impl Rules { @@ -59,7 +66,7 @@ impl Rules { provider: &Provider, ) -> anyhow::Result where - Provider: CatalystSignedDocumentProvider, + Provider: CatalystSignedDocumentProvider + VerifyingKeyProvider, { let rules = [ self.id.check(doc, provider).boxed(), @@ -72,6 +79,7 @@ impl Rules { self.section.check(doc).boxed(), self.parameters.check(doc, provider).boxed(), self.kid.check(doc).boxed(), + self.signature.check(doc, provider).boxed(), ]; let res = futures::future::join_all(rules) @@ -80,6 +88,7 @@ impl Rules { .collect::>>()? .iter() .all(|res| *res); + Ok(res) } } diff --git a/rust/signed_doc/src/validator/rules/parameters.rs b/rust/signed_doc/src/validator/rules/parameters.rs index e26fdcfdc2..3811d48fa7 100644 --- a/rust/signed_doc/src/validator/rules/parameters.rs +++ b/rust/signed_doc/src/validator/rules/parameters.rs @@ -171,8 +171,8 @@ mod tests { use super::*; use crate::{ - builder::tests::Builder, metadata::SupportedField, - providers::tests::TestCatalystSignedDocumentProvider, DocLocator, DocumentRef, + builder::tests::Builder, metadata::SupportedField, providers::tests::TestCatalystProvider, + DocLocator, DocumentRef, }; #[test_case( @@ -712,12 +712,9 @@ mod tests { )] #[tokio::test] async fn parameter_specified_test( - doc_gen: impl FnOnce( - &[DocType; 2], - &mut TestCatalystSignedDocumentProvider, - ) -> CatalystSignedDocument + doc_gen: impl FnOnce(&[DocType; 2], &mut TestCatalystProvider) -> CatalystSignedDocument ) -> bool { - let mut provider = TestCatalystSignedDocumentProvider::default(); + let mut provider = TestCatalystProvider::default(); let exp_param_types: [DocType; 2] = [UuidV4::new().into(), UuidV4::new().into()]; @@ -745,7 +742,7 @@ mod tests { #[tokio::test] async fn ref_specified_optional_test() { - let provider = TestCatalystSignedDocumentProvider::default(); + let provider = TestCatalystProvider::default(); let rule = ParametersRule::Specified { exp_parameters_type: vec![UuidV4::new().into()], optional: true, @@ -754,7 +751,7 @@ mod tests { let doc = Builder::new().build(); assert!(rule.check(&doc, &provider).await.unwrap()); - let provider = TestCatalystSignedDocumentProvider::default(); + let provider = TestCatalystProvider::default(); let rule = ParametersRule::Specified { exp_parameters_type: vec![UuidV4::new().into()], optional: false, @@ -767,7 +764,7 @@ mod tests { #[tokio::test] async fn parameters_rule_not_specified_test() { let rule = ParametersRule::NotSpecified; - let provider = TestCatalystSignedDocumentProvider::default(); + let provider = TestCatalystProvider::default(); let doc = Builder::new().build(); assert!(rule.check(&doc, &provider).await.unwrap()); diff --git a/rust/signed_doc/src/validator/rules/reply.rs b/rust/signed_doc/src/validator/rules/reply.rs index ecbf9b023f..1a77f5d81b 100644 --- a/rust/signed_doc/src/validator/rules/reply.rs +++ b/rust/signed_doc/src/validator/rules/reply.rs @@ -102,9 +102,8 @@ mod tests { use super::*; use crate::{ - builder::tests::Builder, metadata::SupportedField, - providers::tests::TestCatalystSignedDocumentProvider, DocLocator, DocumentRef, - DocumentRefs, + builder::tests::Builder, metadata::SupportedField, providers::tests::TestCatalystProvider, + DocLocator, DocumentRef, DocumentRefs, }; #[test_case( @@ -330,9 +329,9 @@ mod tests { )] #[tokio::test] async fn reply_specified_test( - doc_gen: impl FnOnce(DocType, &mut TestCatalystSignedDocumentProvider) -> CatalystSignedDocument + doc_gen: impl FnOnce(DocType, &mut TestCatalystProvider) -> CatalystSignedDocument ) -> bool { - let mut provider = TestCatalystSignedDocumentProvider::default(); + let mut provider = TestCatalystProvider::default(); let exp_type: DocType = UuidV4::new().into(); @@ -360,7 +359,7 @@ mod tests { #[tokio::test] async fn reply_specified_optional_test() { - let provider = TestCatalystSignedDocumentProvider::default(); + let provider = TestCatalystProvider::default(); let rule = ReplyRule::Specified { exp_reply_type: UuidV4::new().into(), optional: true, @@ -369,7 +368,7 @@ mod tests { let doc = Builder::new().build(); assert!(rule.check(&doc, &provider).await.unwrap()); - let provider = TestCatalystSignedDocumentProvider::default(); + let provider = TestCatalystProvider::default(); let rule = ReplyRule::Specified { exp_reply_type: UuidV4::new().into(), optional: false, @@ -382,7 +381,7 @@ mod tests { #[tokio::test] async fn reply_rule_not_specified_test() { let rule = ReplyRule::NotSpecified; - let provider = TestCatalystSignedDocumentProvider::default(); + let provider = TestCatalystProvider::default(); let doc = Builder::new().build(); assert!(rule.check(&doc, &provider).await.unwrap()); diff --git a/rust/signed_doc/src/validator/rules/signature.rs b/rust/signed_doc/src/validator/rules/signature.rs new file mode 100644 index 0000000000..bb8f92d958 --- /dev/null +++ b/rust/signed_doc/src/validator/rules/signature.rs @@ -0,0 +1,444 @@ +//! Validator for Signatures + +use anyhow::Context; +use catalyst_types::problem_report::ProblemReport; + +use crate::{ + providers::{CatalystSignedDocumentProvider, VerifyingKeyProvider}, + signature::{tbs_data, Signature}, + CatalystSignedDocument, +}; + +/// Signed Document signatures validation rule. +#[derive(Debug)] +pub(crate) struct SignatureRule { + /// Allows multiple signatures. + pub(crate) mutlisig: bool, +} + +impl SignatureRule { + /// Verify document signatures. + /// Return true if all signatures are valid, otherwise return false. + /// + /// # Errors + /// If `provider` returns error, fails fast throwing that error. + pub(crate) async fn check( + &self, + doc: &CatalystSignedDocument, + provider: &Provider, + ) -> anyhow::Result + where + Provider: CatalystSignedDocumentProvider + VerifyingKeyProvider, + { + if doc.signatures().is_empty() { + doc.report().other( + "Catalyst Signed Document is unsigned", + "During Catalyst Signed Document signature validation", + ); + return Ok(false); + } + + if !self.mutlisig && doc.signatures().len() > 1 { + doc.report().other( + format!( + "Multi-signature is not allowed, found {} signatures", + doc.signatures().len() + ) + .as_str(), + "During Catalyst Signed Document signature validation", + ); + return Ok(false); + } + + let sign_rules = doc + .signatures() + .iter() + .map(|sign| validate_signature(doc, sign, provider, doc.report())); + + let res = futures::future::join_all(sign_rules) + .await + .into_iter() + .collect::>>()? + .iter() + .all(|res| *res); + + Ok(res) + } +} + +/// A single signature validation function +async fn validate_signature( + doc: &CatalystSignedDocument, + sign: &Signature, + provider: &Provider, + report: &ProblemReport, +) -> anyhow::Result +where + Provider: VerifyingKeyProvider, +{ + let kid = sign.kid(); + + let Some(pk) = provider.try_get_key(kid).await? else { + report.other( + &format!("Missing public key for {kid}."), + "During public key extraction", + ); + return Ok(false); + }; + + let tbs_data = tbs_data(kid, doc.doc_meta(), doc.content()).context("Probably a bug, cannot build CBOR COSE bytes for signature verification from the structurally valid COSE object.")?; + + let Ok(signature_bytes) = sign.signature().try_into() else { + report.invalid_value( + "cose signature", + &format!("{}", sign.signature().len()), + &format!("must be {}", ed25519_dalek::Signature::BYTE_SIZE), + "During encoding cose signature to bytes", + ); + return Ok(false); + }; + + let signature = ed25519_dalek::Signature::from_bytes(signature_bytes); + if pk.verify_strict(&tbs_data, &signature).is_err() { + report.functional_validation( + &format!("Verification failed for signature with Key ID {kid}"), + "During signature validation with verifying key", + ); + return Ok(false); + } + + Ok(true) +} + +#[cfg(test)] +mod tests { + use std::io::Write; + + use catalyst_types::catalyst_id::role_index::RoleId; + use ed25519_dalek::ed25519::signature::Signer; + + use super::*; + use crate::{providers::tests::*, *}; + + mod helper { + use std::str::FromStr; + + use catalyst_types::catalyst_id::role_index::RoleId; + + use crate::*; + + pub(super) fn create_dummy_key_pair( + role_index: RoleId + ) -> anyhow::Result<( + ed25519_dalek::SigningKey, + ed25519_dalek::VerifyingKey, + CatalystId, + )> { + let sk = create_signing_key(); + let pk = sk.verifying_key(); + let kid = CatalystId::from_str(&format!( + "id.catalyst://cardano/{}/{role_index}/0", + base64_url::encode(pk.as_bytes()) + ))?; + + Ok((sk, pk, kid)) + } + + pub(super) fn create_signing_key() -> ed25519_dalek::SigningKey { + let mut csprng = rand::rngs::OsRng; + ed25519_dalek::SigningKey::generate(&mut csprng) + } + } + + fn metadata() -> serde_json::Value { + serde_json::json!({ + "content-type": ContentType::Json.to_string(), + "content-encoding": ContentEncoding::Brotli.to_string(), + "type": UuidV4::new(), + "id": UuidV7::new(), + "ver": UuidV7::new(), + "ref": {"id": UuidV7::new(), "ver": UuidV7::new()}, + "reply": {"id": UuidV7::new(), "ver": UuidV7::new()}, + "template": {"id": UuidV7::new(), "ver": UuidV7::new()}, + "section": "$", + "collaborators": vec![ + /* cspell:disable */ + "cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE", + "id.catalyst://preprod.cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE/7/3" + /* cspell:enable */ + ], + "parameters": {"id": UuidV7::new(), "ver": UuidV7::new()}, + }) + } + + fn rule(mutlisig: bool) -> SignatureRule { + SignatureRule { mutlisig } + } + + #[tokio::test] + async fn single_signature_validation_test() { + let (sk, pk, kid) = helper::create_dummy_key_pair(RoleId::Role0).unwrap(); + + let signed_doc = Builder::new() + .with_json_metadata(metadata()) + .unwrap() + .with_json_content(&serde_json::Value::Null) + .unwrap() + .add_signature(|m| sk.sign(&m).to_vec(), kid.clone()) + .unwrap() + .build() + .unwrap(); + + assert!(!signed_doc.problem_report().is_problematic()); + + // case: has key + let mut provider = TestCatalystProvider::default(); + provider.add_pk(kid.clone(), pk); + assert!( + rule(true).check(&signed_doc, &provider).await.unwrap(), + "{:?}", + signed_doc.problem_report() + ); + + // case: empty provider + assert!(!rule(true) + .check(&signed_doc, &TestCatalystProvider::default()) + .await + .unwrap()); + + // case: signed with different key + let (another_sk, ..) = helper::create_dummy_key_pair(RoleId::Role0).unwrap(); + let invalid_doc = signed_doc + .into_builder() + .unwrap() + .add_signature(|m| another_sk.sign(&m).to_vec(), kid.clone()) + .unwrap() + .build() + .unwrap(); + assert!(!rule(true).check(&invalid_doc, &provider).await.unwrap()); + + // case: missing signatures + let unsigned_doc = Builder::new() + .with_json_metadata(serde_json::json!({ + "content-type": ContentType::Json.to_string(), + "id": UuidV7::new(), + "ver": UuidV7::new(), + "type": UuidV4::new(), + })) + .unwrap() + .with_json_content(&serde_json::json!({})) + .unwrap() + .build() + .unwrap(); + assert!(!rule(true).check(&unsigned_doc, &provider).await.unwrap()); + } + + #[tokio::test] + async fn multiple_signatures_validation_test() { + let (sk1, pk1, kid1) = helper::create_dummy_key_pair(RoleId::Role0).unwrap(); + let (sk2, pk2, kid2) = helper::create_dummy_key_pair(RoleId::Role0).unwrap(); + let (sk3, pk3, kid3) = helper::create_dummy_key_pair(RoleId::Role0).unwrap(); + let (_, pk_n, kid_n) = helper::create_dummy_key_pair(RoleId::Role0).unwrap(); + + let signed_doc = Builder::new() + .with_json_metadata(metadata()) + .unwrap() + .with_json_content(&serde_json::Value::Null) + .unwrap() + .add_signature(|m| sk1.sign(&m).to_vec(), kid1.clone()) + .unwrap() + .add_signature(|m| sk2.sign(&m).to_vec(), kid2.clone()) + .unwrap() + .add_signature(|m| sk3.sign(&m).to_vec(), kid3.clone()) + .unwrap() + .build() + .unwrap(); + + assert!(!signed_doc.problem_report().is_problematic()); + + // case: multi-sig rule disabled + let mut provider = TestCatalystProvider::default(); + provider.add_pk(kid1.clone(), pk1); + provider.add_pk(kid2.clone(), pk2); + provider.add_pk(kid3.clone(), pk3); + assert!(!rule(false).check(&signed_doc, &provider).await.unwrap()); + + // case: all signatures valid + let mut provider = TestCatalystProvider::default(); + provider.add_pk(kid1.clone(), pk1); + provider.add_pk(kid2.clone(), pk2); + provider.add_pk(kid3.clone(), pk3); + assert!(rule(true).check(&signed_doc, &provider).await.unwrap()); + + // case: partially available signatures + let mut provider = TestCatalystProvider::default(); + provider.add_pk(kid1.clone(), pk1); + provider.add_pk(kid2.clone(), pk2); + assert!(!rule(true).check(&signed_doc, &provider).await.unwrap()); + + // case: with unrecognized provider + let mut provider = TestCatalystProvider::default(); + provider.add_pk(kid_n.clone(), pk_n); + assert!(!rule(true).check(&signed_doc, &provider).await.unwrap()); + + // case: no valid signatures available + assert!(!rule(true) + .check(&signed_doc, &TestCatalystProvider::default()) + .await + .unwrap()); + } + + fn content( + content_bytes: &[u8], + sk: &ed25519_dalek::SigningKey, + kid: &CatalystId, + ) -> anyhow::Result>> { + let mut e = minicbor::Encoder::new(Vec::new()); + e.array(4)?; + // protected headers (empty metadata fields) + let mut m_p_headers = minicbor::Encoder::new(Vec::new()); + m_p_headers.map(0)?; + let m_p_headers = m_p_headers.into_writer(); + e.bytes(m_p_headers.as_slice())?; + // empty unprotected headers + e.map(0)?; + // content + let _ = e.writer_mut().write(content_bytes)?; + // signatures + // one signature + e.array(1)?; + e.array(3)?; + // protected headers (kid field) + let mut s_p_headers = minicbor::Encoder::new(Vec::new()); + s_p_headers + .map(1)? + .u8(4)? + .bytes(Vec::::from(kid).as_slice())?; + let s_p_headers = s_p_headers.into_writer(); + + // [RFC 8152 section 4.4](https://datatracker.ietf.org/doc/html/rfc8152#section-4.4) + let mut tbs: minicbor::Encoder> = minicbor::Encoder::new(Vec::new()); + tbs.array(5)?; + tbs.str("Signature")?; + tbs.bytes(&m_p_headers)?; // `body_protected` + tbs.bytes(&s_p_headers)?; // `sign_protected` + tbs.bytes(&[])?; // empty `external_aad` + tbs.writer_mut().write_all(content_bytes)?; // `payload` + + e.bytes(s_p_headers.as_slice())?; + e.map(0)?; + e.bytes(&sk.sign(tbs.writer()).to_bytes())?; + Ok(e) + } + + fn parameters_alias_field( + alias: &str, + sk: &ed25519_dalek::SigningKey, + kid: &CatalystId, + ) -> anyhow::Result>> { + let mut e = minicbor::Encoder::new(Vec::new()); + e.array(4)?; + // protected headers (empty metadata fields) + let mut m_p_headers = minicbor::Encoder::new(Vec::new()); + m_p_headers.map(0)?; + let m_p_headers = m_p_headers.into_writer(); + e.bytes(m_p_headers.as_slice())?; + // empty unprotected headers + e.map(1)?; + e.str(alias)?.encode_with( + DocumentRef::new(UuidV7::new(), UuidV7::new(), DocLocator::default()), + &mut (), + )?; + // content (random bytes) + let content = [1, 2, 3]; + e.bytes(&content)?; + // signatures + // one signature + e.array(1)?; + e.array(3)?; + // protected headers (kid field) + let mut s_p_headers = minicbor::Encoder::new(Vec::new()); + s_p_headers + .map(1)? + .u8(4)? + .bytes(Vec::::from(kid).as_slice())?; + let s_p_headers = s_p_headers.into_writer(); + + // [RFC 8152 section 4.4](https://datatracker.ietf.org/doc/html/rfc8152#section-4.4) + let mut tbs: minicbor::Encoder> = minicbor::Encoder::new(Vec::new()); + tbs.array(5)?; + tbs.str("Signature")?; + tbs.bytes(&m_p_headers)?; // `body_protected` + tbs.bytes(&s_p_headers)?; // `sign_protected` + tbs.bytes(&[])?; // empty `external_aad` + tbs.bytes(&content)?; // `payload` + + e.bytes(s_p_headers.as_slice())?; + e.map(0)?; + e.bytes(&sk.sign(tbs.writer()).to_bytes())?; + Ok(e) + } + + type DocBytesGenerator = dyn Fn( + &ed25519_dalek::SigningKey, + &CatalystId, + ) -> anyhow::Result>>; + + struct SpecialCborTestCase<'a> { + name: &'static str, + doc_bytes_fn: &'a DocBytesGenerator, + } + + #[tokio::test] + async fn special_cbor_cases() { + let (sk, pk, kid) = helper::create_dummy_key_pair(RoleId::Role0).unwrap(); + let mut provider = TestCatalystProvider::default(); + provider.add_pk(kid.clone(), pk); + + let test_cases: &[SpecialCborTestCase] = &[ + SpecialCborTestCase { + name: "content encoded as cbor null", + doc_bytes_fn: &|sk, kid| { + let mut e = minicbor::Encoder::new(Vec::new()); + content(e.null()?.writer().as_slice(), sk, kid) + }, + }, + SpecialCborTestCase { + name: "content encoded empty bstr e.g. &[]", + doc_bytes_fn: &|sk, kid| { + let mut e = minicbor::Encoder::new(Vec::new()); + content(e.bytes(&[])?.writer().as_slice(), sk, kid) + }, + }, + SpecialCborTestCase { + name: "parameters alias `category_id` field", + doc_bytes_fn: &|sk, kid| parameters_alias_field("category_id", sk, kid), + }, + SpecialCborTestCase { + name: "parameters alias `brand_id` field", + doc_bytes_fn: &|sk, kid| parameters_alias_field("brand_id", sk, kid), + }, + SpecialCborTestCase { + name: "`parameters` alias `campaign_id` field", + doc_bytes_fn: &|sk, kid| parameters_alias_field("campaign_id", sk, kid), + }, + ]; + + for case in test_cases { + let doc = CatalystSignedDocument::try_from( + (case.doc_bytes_fn)(&sk, &kid) + .unwrap() + .into_writer() + .as_slice(), + ) + .unwrap(); + + assert!( + rule(true).check(&doc, &provider).await.unwrap(), + "[case: {}] {:?}", + case.name, + doc.problem_report() + ); + } + } +} diff --git a/rust/signed_doc/src/validator/rules/template.rs b/rust/signed_doc/src/validator/rules/template.rs index 31f88b052a..9d96c9a529 100644 --- a/rust/signed_doc/src/validator/rules/template.rs +++ b/rust/signed_doc/src/validator/rules/template.rs @@ -199,14 +199,14 @@ mod tests { use super::*; use crate::{ - builder::tests::Builder, metadata::SupportedField, - providers::tests::TestCatalystSignedDocumentProvider, DocLocator, DocumentRef, + builder::tests::Builder, metadata::SupportedField, providers::tests::TestCatalystProvider, + DocLocator, DocumentRef, }; #[allow(clippy::too_many_lines)] #[tokio::test] async fn content_rule_templated_test() { - let mut provider = TestCatalystSignedDocumentProvider::default(); + let mut provider = TestCatalystProvider::default(); let exp_template_type = UuidV4::new(); let content_type = ContentType::Json; @@ -425,7 +425,7 @@ mod tests { #[allow(clippy::too_many_lines)] #[tokio::test] async fn content_rule_static_test() { - let provider = TestCatalystSignedDocumentProvider::default(); + let provider = TestCatalystProvider::default(); let schema = json_schema::JsonSchema::try_from(&serde_json::json!({})).unwrap(); let content_schema = ContentSchema::Json(schema); let json_content = serde_json::to_vec(&serde_json::json!({})).unwrap(); @@ -458,7 +458,7 @@ mod tests { #[tokio::test] async fn template_rule_not_specified_test() { let rule = ContentRule::NotSpecified; - let provider = TestCatalystSignedDocumentProvider::default(); + let provider = TestCatalystProvider::default(); let doc = Builder::new().build(); assert!(rule.check(&doc, &provider).await.unwrap()); diff --git a/rust/signed_doc/src/validator/rules/ver.rs b/rust/signed_doc/src/validator/rules/ver.rs index d67aeef11e..1a12bbe371 100644 --- a/rust/signed_doc/src/validator/rules/ver.rs +++ b/rust/signed_doc/src/validator/rules/ver.rs @@ -103,8 +103,8 @@ mod tests { use super::*; use crate::{ - builder::tests::Builder, metadata::SupportedField, - providers::tests::TestCatalystSignedDocumentProvider, UuidV4, UuidV7, + builder::tests::Builder, metadata::SupportedField, providers::tests::TestCatalystProvider, + UuidV4, UuidV7, }; #[test_case( @@ -349,9 +349,9 @@ mod tests { )] #[tokio::test] async fn ver_test( - doc_gen: impl FnOnce(&mut TestCatalystSignedDocumentProvider) -> CatalystSignedDocument + doc_gen: impl FnOnce(&mut TestCatalystProvider) -> CatalystSignedDocument ) -> bool { - let mut provider = TestCatalystSignedDocumentProvider::default(); + let mut provider = TestCatalystProvider::default(); let doc = doc_gen(&mut provider); VerRule.check(&doc, &provider).await.unwrap() diff --git a/rust/signed_doc/tests/comment.rs b/rust/signed_doc/tests/comment.rs index d0f5358911..e41b959d97 100644 --- a/rust/signed_doc/tests/comment.rs +++ b/rust/signed_doc/tests/comment.rs @@ -4,10 +4,7 @@ use std::sync::LazyLock; -use catalyst_signed_doc::{ - providers::tests::{TestCatalystSignedDocumentProvider, TestVerifyingKeyProvider}, - *, -}; +use catalyst_signed_doc::{providers::tests::TestCatalystProvider, *}; use catalyst_types::catalyst_id::role_index::RoleId; use ed25519_dalek::ed25519::signature::Signer; @@ -121,8 +118,8 @@ static COMMENT_REF_DOC: LazyLock = LazyLock::new(|| { #[tokio::test] async fn test_valid_comment_doc() { let (sk, pk, kid) = create_dummy_key_pair(RoleId::Role0).unwrap(); - let mut key_provider = TestVerifyingKeyProvider::default(); - key_provider.add_pk(kid.clone(), pk); + let mut provider = TestCatalystProvider::default(); + provider.add_pk(kid.clone(), pk); // Create a main comment doc, contain all fields mention in the document (except // revocations and section) @@ -159,7 +156,6 @@ async fn test_valid_comment_doc() { .build() .unwrap(); - let mut provider = TestCatalystSignedDocumentProvider::default(); provider.add_document(None, &DUMMY_BRAND_DOC).unwrap(); provider.add_document(None, &DUMMY_PROPOSAL_DOC).unwrap(); provider.add_document(None, &COMMENT_REF_DOC).unwrap(); @@ -167,10 +163,6 @@ async fn test_valid_comment_doc() { let is_valid = validator::validate(&doc, &provider).await.unwrap(); assert!(is_valid, "{:?}", doc.problem_report()); - - let is_valid = validator::validate_signatures(&doc, &key_provider) - .await - .unwrap(); assert!(is_valid); assert!(!doc.problem_report().is_problematic()); } @@ -214,7 +206,7 @@ async fn test_invalid_comment_doc_wrong_role() { .build() .unwrap(); - let mut provider = TestCatalystSignedDocumentProvider::default(); + let mut provider = TestCatalystProvider::default(); provider.add_document(None, &DUMMY_BRAND_DOC).unwrap(); provider.add_document(None, &DUMMY_PROPOSAL_DOC).unwrap(); provider.add_document(None, &COMMENT_REF_DOC).unwrap(); @@ -257,7 +249,7 @@ async fn test_invalid_comment_doc_missing_parameters() { .build() .unwrap(); - let mut provider = TestCatalystSignedDocumentProvider::default(); + let mut provider = TestCatalystProvider::default(); provider.add_document(None, &DUMMY_BRAND_DOC).unwrap(); provider.add_document(None, &DUMMY_PROPOSAL_DOC).unwrap(); provider.add_document(None, &COMMENT_REF_DOC).unwrap(); @@ -300,7 +292,7 @@ async fn test_invalid_comment_doc_missing_template() { .build() .unwrap(); - let mut provider = TestCatalystSignedDocumentProvider::default(); + let mut provider = TestCatalystProvider::default(); provider.add_document(None, &DUMMY_BRAND_DOC).unwrap(); provider.add_document(None, &DUMMY_PROPOSAL_DOC).unwrap(); provider.add_document(None, &COMMENT_REF_DOC).unwrap(); @@ -343,7 +335,7 @@ async fn test_invalid_comment_doc_missing_ref() { .build() .unwrap(); - let mut provider = TestCatalystSignedDocumentProvider::default(); + let mut provider = TestCatalystProvider::default(); provider.add_document(None, &DUMMY_BRAND_DOC).unwrap(); provider.add_document(None, &DUMMY_PROPOSAL_DOC).unwrap(); provider.add_document(None, &COMMENT_REF_DOC).unwrap(); diff --git a/rust/signed_doc/tests/proposal.rs b/rust/signed_doc/tests/proposal.rs index 99019a4cbf..11cf465c6a 100644 --- a/rust/signed_doc/tests/proposal.rs +++ b/rust/signed_doc/tests/proposal.rs @@ -4,10 +4,7 @@ use std::sync::LazyLock; -use catalyst_signed_doc::{ - providers::tests::{TestCatalystSignedDocumentProvider, TestVerifyingKeyProvider}, - *, -}; +use catalyst_signed_doc::{providers::tests::TestCatalystProvider, *}; use catalyst_types::catalyst_id::role_index::RoleId; use ed25519_dalek::ed25519::signature::Signer; @@ -68,8 +65,8 @@ static PROPOSAL_TEMPLATE_DOC: LazyLock = LazyLock::new(| #[tokio::test] async fn test_valid_proposal_doc() { let (sk, pk, kid) = create_dummy_key_pair(RoleId::Proposer).unwrap(); - let mut key_provider = TestVerifyingKeyProvider::default(); - key_provider.add_pk(kid.clone(), pk); + let mut provider = TestCatalystProvider::default(); + provider.add_pk(kid.clone(), pk); // Create a main proposal doc, contain all fields mention in the document (except // collaborations and revocations) @@ -98,17 +95,11 @@ async fn test_valid_proposal_doc() { .build() .unwrap(); - let mut provider = TestCatalystSignedDocumentProvider::default(); - provider.add_document(None, &PROPOSAL_TEMPLATE_DOC).unwrap(); provider.add_document(None, &DUMMY_BRAND_DOC).unwrap(); let is_valid = validator::validate(&doc, &provider).await.unwrap(); assert!(is_valid); - - let is_valid = validator::validate_signatures(&doc, &key_provider) - .await - .unwrap(); assert!(is_valid); assert!(!doc.problem_report().is_problematic()); } @@ -144,7 +135,7 @@ async fn test_invalid_proposal_doc_wrong_role() { .build() .unwrap(); - let mut provider = TestCatalystSignedDocumentProvider::default(); + let mut provider = TestCatalystProvider::default(); provider.add_document(None, &PROPOSAL_TEMPLATE_DOC).unwrap(); provider.add_document(None, &DUMMY_BRAND_DOC).unwrap(); @@ -178,7 +169,7 @@ async fn test_invalid_proposal_doc_missing_template() { .build() .unwrap(); - let mut provider = TestCatalystSignedDocumentProvider::default(); + let mut provider = TestCatalystProvider::default(); provider.add_document(None, &PROPOSAL_TEMPLATE_DOC).unwrap(); provider.add_document(None, &DUMMY_BRAND_DOC).unwrap(); @@ -212,7 +203,7 @@ async fn test_invalid_proposal_doc_missing_parameters() { .build() .unwrap(); - let mut provider = TestCatalystSignedDocumentProvider::default(); + let mut provider = TestCatalystProvider::default(); provider.add_document(None, &PROPOSAL_TEMPLATE_DOC).unwrap(); provider.add_document(None, &DUMMY_BRAND_DOC).unwrap(); diff --git a/rust/signed_doc/tests/signature.rs b/rust/signed_doc/tests/signature.rs deleted file mode 100644 index f79e9d934b..0000000000 --- a/rust/signed_doc/tests/signature.rs +++ /dev/null @@ -1,306 +0,0 @@ -//! Integration test for signature validation part. - -use std::io::Write; - -use catalyst_signed_doc::{providers::tests::TestVerifyingKeyProvider, *}; -use catalyst_types::catalyst_id::role_index::RoleId; -use ed25519_dalek::ed25519::signature::Signer; - -use crate::common::create_dummy_key_pair; - -mod common; - -fn metadata() -> serde_json::Value { - serde_json::json!({ - "content-type": ContentType::Json.to_string(), - "content-encoding": ContentEncoding::Brotli.to_string(), - "type": UuidV4::new(), - "id": UuidV7::new(), - "ver": UuidV7::new(), - "ref": {"id": UuidV7::new(), "ver": UuidV7::new()}, - "reply": {"id": UuidV7::new(), "ver": UuidV7::new()}, - "template": {"id": UuidV7::new(), "ver": UuidV7::new()}, - "section": "$", - "collaborators": vec![ - /* cspell:disable */ - "cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE", - "id.catalyst://preprod.cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE/7/3" - /* cspell:enable */ - ], - "parameters": {"id": UuidV7::new(), "ver": UuidV7::new()}, - }) -} - -#[tokio::test] -async fn single_signature_validation_test() { - let (sk, pk, kid) = create_dummy_key_pair(RoleId::Role0).unwrap(); - - let signed_doc = Builder::new() - .with_json_metadata(metadata()) - .unwrap() - .with_json_content(&serde_json::Value::Null) - .unwrap() - .add_signature(|m| sk.sign(&m).to_vec(), kid.clone()) - .unwrap() - .build() - .unwrap(); - - assert!(!signed_doc.problem_report().is_problematic()); - - // case: has key - let mut provider = TestVerifyingKeyProvider::default(); - provider.add_pk(kid.clone(), pk); - assert!( - validator::validate_signatures(&signed_doc, &provider) - .await - .unwrap(), - "{:?}", - signed_doc.problem_report() - ); - - // case: empty provider - assert!( - !validator::validate_signatures(&signed_doc, &TestVerifyingKeyProvider::default()) - .await - .unwrap() - ); - - // case: signed with different key - let (another_sk, ..) = create_dummy_key_pair(RoleId::Role0).unwrap(); - let invalid_doc = signed_doc - .into_builder() - .unwrap() - .add_signature(|m| another_sk.sign(&m).to_vec(), kid.clone()) - .unwrap() - .build() - .unwrap(); - assert!(!validator::validate_signatures(&invalid_doc, &provider) - .await - .unwrap()); - - // case: missing signatures - let unsigned_doc = Builder::new() - .with_json_metadata(serde_json::json!({ - "content-type": ContentType::Json.to_string(), - "id": UuidV7::new(), - "ver": UuidV7::new(), - "type": UuidV4::new(), - })) - .unwrap() - .with_json_content(&serde_json::json!({})) - .unwrap() - .build() - .unwrap(); - assert!(!validator::validate_signatures(&unsigned_doc, &provider) - .await - .unwrap()); -} - -#[tokio::test] -async fn multiple_signatures_validation_test() { - let (sk1, pk1, kid1) = common::create_dummy_key_pair(RoleId::Role0).unwrap(); - let (sk2, pk2, kid2) = common::create_dummy_key_pair(RoleId::Role0).unwrap(); - let (sk3, pk3, kid3) = common::create_dummy_key_pair(RoleId::Role0).unwrap(); - let (_, pk_n, kid_n) = common::create_dummy_key_pair(RoleId::Role0).unwrap(); - - let signed_doc = Builder::new() - .with_json_metadata(metadata()) - .unwrap() - .with_json_content(&serde_json::Value::Null) - .unwrap() - .add_signature(|m| sk1.sign(&m).to_vec(), kid1.clone()) - .unwrap() - .add_signature(|m| sk2.sign(&m).to_vec(), kid2.clone()) - .unwrap() - .add_signature(|m| sk3.sign(&m).to_vec(), kid3.clone()) - .unwrap() - .build() - .unwrap(); - - assert!(!signed_doc.problem_report().is_problematic()); - - // case: all signatures valid - let mut provider = TestVerifyingKeyProvider::default(); - provider.add_pk(kid1.clone(), pk1); - provider.add_pk(kid2.clone(), pk2); - provider.add_pk(kid3.clone(), pk3); - assert!(validator::validate_signatures(&signed_doc, &provider) - .await - .unwrap()); - - // case: partially available signatures - let mut provider = TestVerifyingKeyProvider::default(); - provider.add_pk(kid1.clone(), pk1); - provider.add_pk(kid2.clone(), pk2); - assert!(!validator::validate_signatures(&signed_doc, &provider) - .await - .unwrap()); - - // case: with unrecognized provider - let mut provider = TestVerifyingKeyProvider::default(); - provider.add_pk(kid_n.clone(), pk_n); - assert!(!validator::validate_signatures(&signed_doc, &provider) - .await - .unwrap()); - - // case: no valid signatures available - assert!( - !validator::validate_signatures(&signed_doc, &TestVerifyingKeyProvider::default()) - .await - .unwrap() - ); -} - -fn content( - content_bytes: &[u8], - sk: &ed25519_dalek::SigningKey, - kid: &CatalystId, -) -> anyhow::Result>> { - let mut e = minicbor::Encoder::new(Vec::new()); - e.array(4)?; - // protected headers (empty metadata fields) - let mut m_p_headers = minicbor::Encoder::new(Vec::new()); - m_p_headers.map(0)?; - let m_p_headers = m_p_headers.into_writer(); - e.bytes(m_p_headers.as_slice())?; - // empty unprotected headers - e.map(0)?; - // content - let _ = e.writer_mut().write(content_bytes)?; - // signatures - // one signature - e.array(1)?; - e.array(3)?; - // protected headers (kid field) - let mut s_p_headers = minicbor::Encoder::new(Vec::new()); - s_p_headers - .map(1)? - .u8(4)? - .bytes(Vec::::from(kid).as_slice())?; - let s_p_headers = s_p_headers.into_writer(); - - // [RFC 8152 section 4.4](https://datatracker.ietf.org/doc/html/rfc8152#section-4.4) - let mut tbs: minicbor::Encoder> = minicbor::Encoder::new(Vec::new()); - tbs.array(5)?; - tbs.str("Signature")?; - tbs.bytes(&m_p_headers)?; // `body_protected` - tbs.bytes(&s_p_headers)?; // `sign_protected` - tbs.bytes(&[])?; // empty `external_aad` - tbs.writer_mut().write_all(content_bytes)?; // `payload` - - e.bytes(s_p_headers.as_slice())?; - e.map(0)?; - e.bytes(&sk.sign(tbs.writer()).to_bytes())?; - Ok(e) -} - -fn parameters_alias_field( - alias: &str, - sk: &ed25519_dalek::SigningKey, - kid: &CatalystId, -) -> anyhow::Result>> { - let mut e = minicbor::Encoder::new(Vec::new()); - e.array(4)?; - // protected headers (empty metadata fields) - let mut m_p_headers = minicbor::Encoder::new(Vec::new()); - m_p_headers.map(0)?; - let m_p_headers = m_p_headers.into_writer(); - e.bytes(m_p_headers.as_slice())?; - // empty unprotected headers - e.map(1)?; - e.str(alias)?.encode_with( - DocumentRef::new(UuidV7::new(), UuidV7::new(), DocLocator::default()), - &mut (), - )?; - // content (random bytes) - let content = [1, 2, 3]; - e.bytes(&content)?; - // signatures - // one signature - e.array(1)?; - e.array(3)?; - // protected headers (kid field) - let mut s_p_headers = minicbor::Encoder::new(Vec::new()); - s_p_headers - .map(1)? - .u8(4)? - .bytes(Vec::::from(kid).as_slice())?; - let s_p_headers = s_p_headers.into_writer(); - - // [RFC 8152 section 4.4](https://datatracker.ietf.org/doc/html/rfc8152#section-4.4) - let mut tbs: minicbor::Encoder> = minicbor::Encoder::new(Vec::new()); - tbs.array(5)?; - tbs.str("Signature")?; - tbs.bytes(&m_p_headers)?; // `body_protected` - tbs.bytes(&s_p_headers)?; // `sign_protected` - tbs.bytes(&[])?; // empty `external_aad` - tbs.bytes(&content)?; // `payload` - - e.bytes(s_p_headers.as_slice())?; - e.map(0)?; - e.bytes(&sk.sign(tbs.writer()).to_bytes())?; - Ok(e) -} - -type DocBytesGenerator = - dyn Fn(&ed25519_dalek::SigningKey, &CatalystId) -> anyhow::Result>>; - -struct SpecialCborTestCase<'a> { - name: &'static str, - doc_bytes_fn: &'a DocBytesGenerator, -} - -#[tokio::test] -async fn special_cbor_cases() { - let (sk, pk, kid) = create_dummy_key_pair(RoleId::Role0).unwrap(); - let mut provider = TestVerifyingKeyProvider::default(); - provider.add_pk(kid.clone(), pk); - - let test_cases: &[SpecialCborTestCase] = &[ - SpecialCborTestCase { - name: "content encoded as cbor null", - doc_bytes_fn: &|sk, kid| { - let mut e = minicbor::Encoder::new(Vec::new()); - content(e.null()?.writer().as_slice(), sk, kid) - }, - }, - SpecialCborTestCase { - name: "content encoded empty bstr e.g. &[]", - doc_bytes_fn: &|sk, kid| { - let mut e = minicbor::Encoder::new(Vec::new()); - content(e.bytes(&[])?.writer().as_slice(), sk, kid) - }, - }, - SpecialCborTestCase { - name: "parameters alias `category_id` field", - doc_bytes_fn: &|sk, kid| parameters_alias_field("category_id", sk, kid), - }, - SpecialCborTestCase { - name: "parameters alias `brand_id` field", - doc_bytes_fn: &|sk, kid| parameters_alias_field("brand_id", sk, kid), - }, - SpecialCborTestCase { - name: "`parameters` alias `campaign_id` field", - doc_bytes_fn: &|sk, kid| parameters_alias_field("campaign_id", sk, kid), - }, - ]; - - for case in test_cases { - let doc = CatalystSignedDocument::try_from( - (case.doc_bytes_fn)(&sk, &kid) - .unwrap() - .into_writer() - .as_slice(), - ) - .unwrap(); - - assert!( - validator::validate_signatures(&doc, &provider) - .await - .unwrap(), - "[case: {}] {:?}", - case.name, - doc.problem_report() - ); - } -} diff --git a/rust/signed_doc/tests/submission.rs b/rust/signed_doc/tests/submission.rs index 9d72045bc5..8c4e4c2a48 100644 --- a/rust/signed_doc/tests/submission.rs +++ b/rust/signed_doc/tests/submission.rs @@ -4,10 +4,7 @@ use std::sync::LazyLock; -use catalyst_signed_doc::{ - providers::tests::{TestCatalystSignedDocumentProvider, TestVerifyingKeyProvider}, - *, -}; +use catalyst_signed_doc::{providers::tests::TestCatalystProvider, *}; use catalyst_types::catalyst_id::role_index::RoleId; use ed25519_dalek::ed25519::signature::Signer; @@ -61,8 +58,8 @@ static DUMMY_BRAND_DOC: LazyLock = LazyLock::new(|| { #[tokio::test] async fn test_valid_submission_action() { let (sk, pk, kid) = create_dummy_key_pair(RoleId::Proposer).unwrap(); - let mut key_provider = TestVerifyingKeyProvider::default(); - key_provider.add_pk(kid.clone(), pk); + let mut provider = TestCatalystProvider::default(); + provider.add_pk(kid.clone(), pk); // Create a main proposal submission doc, contain all fields mention in the document let id = UuidV7::new(); @@ -92,17 +89,11 @@ async fn test_valid_submission_action() { .build() .unwrap(); - let mut provider = TestCatalystSignedDocumentProvider::default(); - provider.add_document(None, &DUMMY_PROPOSAL_DOC).unwrap(); provider.add_document(None, &DUMMY_BRAND_DOC).unwrap(); let is_valid = validator::validate(&doc, &provider).await.unwrap(); assert!(is_valid, "{:?}", doc.problem_report()); - - let is_valid = validator::validate_signatures(&doc, &key_provider) - .await - .unwrap(); assert!(is_valid); assert!(!doc.problem_report().is_problematic()); } @@ -139,7 +130,7 @@ async fn test_invalid_submission_action_wrong_role() { .build() .unwrap(); - let mut provider = TestCatalystSignedDocumentProvider::default(); + let mut provider = TestCatalystProvider::default(); provider.add_document(None, &DUMMY_PROPOSAL_DOC).unwrap(); provider.add_document(None, &DUMMY_BRAND_DOC).unwrap(); @@ -173,7 +164,7 @@ async fn test_invalid_submission_action_corrupted_json() { .build() .unwrap(); - let mut provider = TestCatalystSignedDocumentProvider::default(); + let mut provider = TestCatalystProvider::default(); provider.add_document(None, &DUMMY_PROPOSAL_DOC).unwrap(); provider.add_document(None, &DUMMY_BRAND_DOC).unwrap(); @@ -209,7 +200,7 @@ async fn test_invalid_submission_action_missing_ref() { .build() .unwrap(); - let mut provider = TestCatalystSignedDocumentProvider::default(); + let mut provider = TestCatalystProvider::default(); provider.add_document(None, &DUMMY_PROPOSAL_DOC).unwrap(); provider.add_document(None, &DUMMY_BRAND_DOC).unwrap(); @@ -245,7 +236,7 @@ async fn test_invalid_submission_action_missing_parameters() { .build() .unwrap(); - let mut provider = TestCatalystSignedDocumentProvider::default(); + let mut provider = TestCatalystProvider::default(); provider.add_document(None, &DUMMY_PROPOSAL_DOC).unwrap(); provider.add_document(None, &DUMMY_BRAND_DOC).unwrap();