diff --git a/docs/src/architecture/08_concepts/signed_doc/cddl/collaborators.cddl b/docs/src/architecture/08_concepts/signed_doc/cddl/collaborators.cddl index 410b614aeaa..9aab4024c28 100644 --- a/docs/src/architecture/08_concepts/signed_doc/cddl/collaborators.cddl +++ b/docs/src/architecture/08_concepts/signed_doc/cddl/collaborators.cddl @@ -2,7 +2,7 @@ ; Allowed Collaborators on the next subsequent version of a document. -collaborators = [ * catalyst_id_kid ] +collaborators = [ + catalyst_id_kid ] ; UTF8 Catalyst ID URI encoded as a bytes string. catalyst_id_kid = bytes diff --git a/docs/src/architecture/08_concepts/signed_doc/cddl/signed_document.cddl b/docs/src/architecture/08_concepts/signed_doc/cddl/signed_document.cddl index 947b56a8db7..47a41a0ccab 100644 --- a/docs/src/architecture/08_concepts/signed_doc/cddl/signed_document.cddl +++ b/docs/src/architecture/08_concepts/signed_doc/cddl/signed_document.cddl @@ -116,7 +116,7 @@ section_ref = json_pointer json_pointer = text ; Allowed Collaborators on the next subsequent version of a document. -collaborators = [ * catalyst_id_kid ] +collaborators = [ + catalyst_id_kid ] ; UTF8 Catalyst ID URI encoded as a bytes string. catalyst_id_kid = bytes diff --git a/docs/src/architecture/08_concepts/signed_doc/docs/brand_parameters.md b/docs/src/architecture/08_concepts/signed_doc/docs/brand_parameters.md index bbe3a606730..48c6144f138 100644 --- a/docs/src/architecture/08_concepts/signed_doc/docs/brand_parameters.md +++ b/docs/src/architecture/08_concepts/signed_doc/docs/brand_parameters.md @@ -197,7 +197,7 @@ New versions of this document may be published by: | --- | --- | | License | This document is licensed under [CC-BY-4.0] | | Created | 2024-12-27 | -| Modified | 2025-09-09 | +| Modified | 2025-10-17 | | Authors | Alex Pozhylenkov | | | Nathan Bogale | | | Neil McAuliffe | diff --git a/docs/src/architecture/08_concepts/signed_doc/docs/brand_parameters_form_template.md b/docs/src/architecture/08_concepts/signed_doc/docs/brand_parameters_form_template.md index 16bc5597e49..c5ef62d8897 100644 --- a/docs/src/architecture/08_concepts/signed_doc/docs/brand_parameters_form_template.md +++ b/docs/src/architecture/08_concepts/signed_doc/docs/brand_parameters_form_template.md @@ -133,7 +133,7 @@ New versions of this document may be published by: | --- | --- | | License | This document is licensed under [CC-BY-4.0] | | Created | 2024-12-27 | -| Modified | 2025-09-09 | +| Modified | 2025-10-17 | | Authors | Alex Pozhylenkov | | | Nathan Bogale | | | Neil McAuliffe | diff --git a/docs/src/architecture/08_concepts/signed_doc/docs/campaign_parameters.md b/docs/src/architecture/08_concepts/signed_doc/docs/campaign_parameters.md index 0009dc515ff..b6842ea0164 100644 --- a/docs/src/architecture/08_concepts/signed_doc/docs/campaign_parameters.md +++ b/docs/src/architecture/08_concepts/signed_doc/docs/campaign_parameters.md @@ -226,7 +226,7 @@ New versions of this document may be published by: | --- | --- | | License | This document is licensed under [CC-BY-4.0] | | Created | 2024-12-27 | -| Modified | 2025-09-09 | +| Modified | 2025-10-17 | | Authors | Alex Pozhylenkov | | | Nathan Bogale | | | Neil McAuliffe | diff --git a/docs/src/architecture/08_concepts/signed_doc/docs/campaign_parameters_form_template.md b/docs/src/architecture/08_concepts/signed_doc/docs/campaign_parameters_form_template.md index 0b6629d93d6..9c855ef62a5 100644 --- a/docs/src/architecture/08_concepts/signed_doc/docs/campaign_parameters_form_template.md +++ b/docs/src/architecture/08_concepts/signed_doc/docs/campaign_parameters_form_template.md @@ -161,7 +161,7 @@ New versions of this document may be published by: | --- | --- | | License | This document is licensed under [CC-BY-4.0] | | Created | 2024-12-27 | -| Modified | 2025-09-09 | +| Modified | 2025-10-17 | | Authors | Alex Pozhylenkov | | | Nathan Bogale | | | Neil McAuliffe | diff --git a/docs/src/architecture/08_concepts/signed_doc/docs/category_parameters.md b/docs/src/architecture/08_concepts/signed_doc/docs/category_parameters.md index 94def30d664..69949a770a7 100644 --- a/docs/src/architecture/08_concepts/signed_doc/docs/category_parameters.md +++ b/docs/src/architecture/08_concepts/signed_doc/docs/category_parameters.md @@ -226,7 +226,7 @@ New versions of this document may be published by: | --- | --- | | License | This document is licensed under [CC-BY-4.0] | | Created | 2024-12-27 | -| Modified | 2025-09-09 | +| Modified | 2025-10-17 | | Authors | Alex Pozhylenkov | | | Nathan Bogale | | | Neil McAuliffe | diff --git a/docs/src/architecture/08_concepts/signed_doc/docs/category_parameters_form_template.md b/docs/src/architecture/08_concepts/signed_doc/docs/category_parameters_form_template.md index 77dce48c25e..430e238f927 100644 --- a/docs/src/architecture/08_concepts/signed_doc/docs/category_parameters_form_template.md +++ b/docs/src/architecture/08_concepts/signed_doc/docs/category_parameters_form_template.md @@ -161,7 +161,7 @@ New versions of this document may be published by: | --- | --- | | License | This document is licensed under [CC-BY-4.0] | | Created | 2024-12-27 | -| Modified | 2025-09-09 | +| Modified | 2025-10-17 | | Authors | Alex Pozhylenkov | | | Nathan Bogale | | | Neil McAuliffe | diff --git a/docs/src/architecture/08_concepts/signed_doc/docs/comment_moderation_action.md b/docs/src/architecture/08_concepts/signed_doc/docs/comment_moderation_action.md index 37bc36cbaef..811b5563bea 100644 --- a/docs/src/architecture/08_concepts/signed_doc/docs/comment_moderation_action.md +++ b/docs/src/architecture/08_concepts/signed_doc/docs/comment_moderation_action.md @@ -161,7 +161,7 @@ New versions of this document may be published by: | --- | --- | | License | This document is licensed under [CC-BY-4.0] | | Created | 2024-12-27 | -| Modified | 2025-09-09 | +| Modified | 2025-10-17 | | Authors | Alex Pozhylenkov | | | Nathan Bogale | | | Neil McAuliffe | diff --git a/docs/src/architecture/08_concepts/signed_doc/docs/contest_delegation.md b/docs/src/architecture/08_concepts/signed_doc/docs/contest_delegation.md index 40b42a7594c..be9f20a349d 100644 --- a/docs/src/architecture/08_concepts/signed_doc/docs/contest_delegation.md +++ b/docs/src/architecture/08_concepts/signed_doc/docs/contest_delegation.md @@ -252,7 +252,7 @@ New versions of this document may be published by: | --- | --- | | License | This document is licensed under [CC-BY-4.0] | | Created | 2024-12-27 | -| Modified | 2025-09-09 | +| Modified | 2025-10-17 | | Authors | Alex Pozhylenkov | | | Nathan Bogale | | | Neil McAuliffe | diff --git a/docs/src/architecture/08_concepts/signed_doc/docs/contest_parameters.md b/docs/src/architecture/08_concepts/signed_doc/docs/contest_parameters.md index d7e3e90564a..604ff3e2f0e 100644 --- a/docs/src/architecture/08_concepts/signed_doc/docs/contest_parameters.md +++ b/docs/src/architecture/08_concepts/signed_doc/docs/contest_parameters.md @@ -228,7 +228,7 @@ New versions of this document may be published by: | --- | --- | | License | This document is licensed under [CC-BY-4.0] | | Created | 2024-12-27 | -| Modified | 2025-09-09 | +| Modified | 2025-10-17 | | Authors | Alex Pozhylenkov | | | Nathan Bogale | | | Neil McAuliffe | diff --git a/docs/src/architecture/08_concepts/signed_doc/docs/contest_parameters_form_template.md b/docs/src/architecture/08_concepts/signed_doc/docs/contest_parameters_form_template.md index 17044a96602..9daecae61d4 100644 --- a/docs/src/architecture/08_concepts/signed_doc/docs/contest_parameters_form_template.md +++ b/docs/src/architecture/08_concepts/signed_doc/docs/contest_parameters_form_template.md @@ -163,7 +163,7 @@ New versions of this document may be published by: | --- | --- | | License | This document is licensed under [CC-BY-4.0] | | Created | 2024-12-27 | -| Modified | 2025-09-09 | +| Modified | 2025-10-17 | | Authors | Alex Pozhylenkov | | | Nathan Bogale | | | Neil McAuliffe | diff --git a/docs/src/architecture/08_concepts/signed_doc/docs/presentation_template.md b/docs/src/architecture/08_concepts/signed_doc/docs/presentation_template.md index 1eff632197e..7e1a901c359 100644 --- a/docs/src/architecture/08_concepts/signed_doc/docs/presentation_template.md +++ b/docs/src/architecture/08_concepts/signed_doc/docs/presentation_template.md @@ -257,7 +257,7 @@ New versions of this document may be published by: | --- | --- | | License | This document is licensed under [CC-BY-4.0] | | Created | 2024-12-27 | -| Modified | 2025-09-09 | +| Modified | 2025-10-17 | | Authors | Alex Pozhylenkov | | | Nathan Bogale | | | Neil McAuliffe | diff --git a/docs/src/architecture/08_concepts/signed_doc/docs/proposal.md b/docs/src/architecture/08_concepts/signed_doc/docs/proposal.md index e1b75d34295..f155f7ee20e 100644 --- a/docs/src/architecture/08_concepts/signed_doc/docs/proposal.md +++ b/docs/src/architecture/08_concepts/signed_doc/docs/proposal.md @@ -240,7 +240,7 @@ New versions of this document may be published by: | --- | --- | | License | This document is licensed under [CC-BY-4.0] | | Created | 2024-12-27 | -| Modified | 2025-09-09 | +| Modified | 2025-10-17 | | Authors | Alex Pozhylenkov | | | Nathan Bogale | | | Neil McAuliffe | diff --git a/docs/src/architecture/08_concepts/signed_doc/docs/proposal_comment.md b/docs/src/architecture/08_concepts/signed_doc/docs/proposal_comment.md index 10b52d51b7b..3f1c28e0d89 100644 --- a/docs/src/architecture/08_concepts/signed_doc/docs/proposal_comment.md +++ b/docs/src/architecture/08_concepts/signed_doc/docs/proposal_comment.md @@ -276,7 +276,7 @@ New versions of this document may be published by: | --- | --- | | License | This document is licensed under [CC-BY-4.0] | | Created | 2024-12-27 | -| Modified | 2025-09-09 | +| Modified | 2025-10-17 | | Authors | Alex Pozhylenkov | | | Nathan Bogale | | | Neil McAuliffe | diff --git a/docs/src/architecture/08_concepts/signed_doc/docs/proposal_comment_form_template.md b/docs/src/architecture/08_concepts/signed_doc/docs/proposal_comment_form_template.md index 1458e01553f..f5a8d8444ed 100644 --- a/docs/src/architecture/08_concepts/signed_doc/docs/proposal_comment_form_template.md +++ b/docs/src/architecture/08_concepts/signed_doc/docs/proposal_comment_form_template.md @@ -163,7 +163,7 @@ New versions of this document may be published by: | --- | --- | | License | This document is licensed under [CC-BY-4.0] | | Created | 2024-12-27 | -| Modified | 2025-09-09 | +| Modified | 2025-10-17 | | Authors | Alex Pozhylenkov | | | Nathan Bogale | | | Neil McAuliffe | diff --git a/docs/src/architecture/08_concepts/signed_doc/docs/proposal_form_template.md b/docs/src/architecture/08_concepts/signed_doc/docs/proposal_form_template.md index e94db568e6f..7abac0bc288 100644 --- a/docs/src/architecture/08_concepts/signed_doc/docs/proposal_form_template.md +++ b/docs/src/architecture/08_concepts/signed_doc/docs/proposal_form_template.md @@ -163,7 +163,7 @@ New versions of this document may be published by: | --- | --- | | License | This document is licensed under [CC-BY-4.0] | | Created | 2024-12-27 | -| Modified | 2025-09-09 | +| Modified | 2025-10-17 | | Authors | Alex Pozhylenkov | | | Nathan Bogale | | | Neil McAuliffe | diff --git a/docs/src/architecture/08_concepts/signed_doc/docs/proposal_moderation_action.md b/docs/src/architecture/08_concepts/signed_doc/docs/proposal_moderation_action.md index 00c8871649f..441a3398907 100644 --- a/docs/src/architecture/08_concepts/signed_doc/docs/proposal_moderation_action.md +++ b/docs/src/architecture/08_concepts/signed_doc/docs/proposal_moderation_action.md @@ -161,7 +161,7 @@ New versions of this document may be published by: | --- | --- | | License | This document is licensed under [CC-BY-4.0] | | Created | 2024-12-27 | -| Modified | 2025-09-09 | +| Modified | 2025-10-17 | | Authors | Alex Pozhylenkov | | | Nathan Bogale | | | Neil McAuliffe | diff --git a/docs/src/architecture/08_concepts/signed_doc/docs/proposal_submission_action.md b/docs/src/architecture/08_concepts/signed_doc/docs/proposal_submission_action.md index 86156cea1c1..40aa3db3d6c 100644 --- a/docs/src/architecture/08_concepts/signed_doc/docs/proposal_submission_action.md +++ b/docs/src/architecture/08_concepts/signed_doc/docs/proposal_submission_action.md @@ -330,7 +330,7 @@ New versions of this document may be published by: | --- | --- | | License | This document is licensed under [CC-BY-4.0] | | Created | 2024-12-27 | -| Modified | 2025-09-09 | +| Modified | 2025-10-17 | | Authors | Alex Pozhylenkov | | | Nathan Bogale | | | Neil McAuliffe | diff --git a/docs/src/architecture/08_concepts/signed_doc/docs/rep_nomination.md b/docs/src/architecture/08_concepts/signed_doc/docs/rep_nomination.md index cf6e7199c25..67d6a4d2e30 100644 --- a/docs/src/architecture/08_concepts/signed_doc/docs/rep_nomination.md +++ b/docs/src/architecture/08_concepts/signed_doc/docs/rep_nomination.md @@ -282,7 +282,7 @@ New versions of this document may be published by: | --- | --- | | License | This document is licensed under [CC-BY-4.0] | | Created | 2024-12-27 | -| Modified | 2025-09-09 | +| Modified | 2025-10-17 | | Authors | Alex Pozhylenkov | | | Nathan Bogale | | | Neil McAuliffe | diff --git a/docs/src/architecture/08_concepts/signed_doc/docs/rep_nomination_form_template.md b/docs/src/architecture/08_concepts/signed_doc/docs/rep_nomination_form_template.md index 7637f2af8a7..18b4e517a10 100644 --- a/docs/src/architecture/08_concepts/signed_doc/docs/rep_nomination_form_template.md +++ b/docs/src/architecture/08_concepts/signed_doc/docs/rep_nomination_form_template.md @@ -161,7 +161,7 @@ New versions of this document may be published by: | --- | --- | | License | This document is licensed under [CC-BY-4.0] | | Created | 2024-12-27 | -| Modified | 2025-09-09 | +| Modified | 2025-10-17 | | Authors | Alex Pozhylenkov | | | Nathan Bogale | | | Neil McAuliffe | diff --git a/docs/src/architecture/08_concepts/signed_doc/docs/rep_profile.md b/docs/src/architecture/08_concepts/signed_doc/docs/rep_profile.md index 4200afdc8c7..4ee04771ccd 100644 --- a/docs/src/architecture/08_concepts/signed_doc/docs/rep_profile.md +++ b/docs/src/architecture/08_concepts/signed_doc/docs/rep_profile.md @@ -197,7 +197,7 @@ New versions of this document may be published by: | --- | --- | | License | This document is licensed under [CC-BY-4.0] | | Created | 2024-12-27 | -| Modified | 2025-09-09 | +| Modified | 2025-10-17 | | Authors | Alex Pozhylenkov | | | Nathan Bogale | | | Neil McAuliffe | diff --git a/docs/src/architecture/08_concepts/signed_doc/docs/rep_profile_form_template.md b/docs/src/architecture/08_concepts/signed_doc/docs/rep_profile_form_template.md index 63322ada6f5..44408def0dc 100644 --- a/docs/src/architecture/08_concepts/signed_doc/docs/rep_profile_form_template.md +++ b/docs/src/architecture/08_concepts/signed_doc/docs/rep_profile_form_template.md @@ -161,7 +161,7 @@ New versions of this document may be published by: | --- | --- | | License | This document is licensed under [CC-BY-4.0] | | Created | 2024-12-27 | -| Modified | 2025-09-09 | +| Modified | 2025-10-17 | | Authors | Alex Pozhylenkov | | | Nathan Bogale | | | Neil McAuliffe | diff --git a/docs/src/architecture/08_concepts/signed_doc/metadata.md b/docs/src/architecture/08_concepts/signed_doc/metadata.md index 4c4d3a8c509..21dcdec17cd 100644 --- a/docs/src/architecture/08_concepts/signed_doc/metadata.md +++ b/docs/src/architecture/08_concepts/signed_doc/metadata.md @@ -618,7 +618,7 @@ classDiagram | --- | --- | | License | This document is licensed under [CC-BY-4.0] | | Created | 2024-12-27 | -| Modified | 2025-09-09 | +| Modified | 2025-10-17 | | Authors | Alex Pozhylenkov | | | Nathan Bogale | | | Neil McAuliffe | diff --git a/docs/src/architecture/08_concepts/signed_doc/spec.md b/docs/src/architecture/08_concepts/signed_doc/spec.md index 70becaacbde..0d05c571b67 100644 --- a/docs/src/architecture/08_concepts/signed_doc/spec.md +++ b/docs/src/architecture/08_concepts/signed_doc/spec.md @@ -549,7 +549,7 @@ Catalyst ID URI iden | --- | --- | | License | This document is licensed under [CC-BY-4.0] | | Created | 2024-12-27 | -| Modified | 2025-09-09 | +| Modified | 2025-10-17 | | Authors | Alex Pozhylenkov | | | Nathan Bogale | | | Neil McAuliffe | @@ -608,6 +608,10 @@ Catalyst ID URI iden * Fixed an invalid 'Presentation Template' [JSON schema][JSON Schema-2020-12]. +#### 0.1.4 (2025-10-17) + +* Modified [`collaborators`](metadata.md#collaborators) [cddl][RFC8610] definition, it must have at least one element in array. + [CBOR-LFD-ENCODING]: https://www.rfc-editor.org/rfc/rfc8949.html#section-4.2.3 [RFC9052-HeaderParameters]: https://www.rfc-editor.org/rfc/rfc8152#section-3.1 [JSON Schema-2020-12]: https://json-schema.org/draft/2020-12 diff --git a/docs/src/architecture/08_concepts/signed_doc/types.md b/docs/src/architecture/08_concepts/signed_doc/types.md index edd56951243..05c42fdbfe3 100644 --- a/docs/src/architecture/08_concepts/signed_doc/types.md +++ b/docs/src/architecture/08_concepts/signed_doc/types.md @@ -40,7 +40,7 @@ All Defined Document Types | --- | --- | | License | This document is licensed under [CC-BY-4.0] | | Created | 2024-12-27 | -| Modified | 2025-09-09 | +| Modified | 2025-10-17 | | Authors | Alex Pozhylenkov | | | Nathan Bogale | | | Neil McAuliffe | diff --git a/rust/catalyst-signed-doc-spec/Cargo.toml b/rust/catalyst-signed-doc-spec/Cargo.toml index 31e8c061c6e..ded0be4985f 100644 --- a/rust/catalyst-signed-doc-spec/Cargo.toml +++ b/rust/catalyst-signed-doc-spec/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "catalyst-signed-doc-spec" -version = "0.1.3" +version = "0.1.4" edition.workspace = true authors.workspace = true homepage.workspace = true diff --git a/rust/signed_doc/src/lib.rs b/rust/signed_doc/src/lib.rs index 5fa17f895e6..16343d5df63 100644 --- a/rust/signed_doc/src/lib.rs +++ b/rust/signed_doc/src/lib.rs @@ -239,6 +239,27 @@ impl CatalystSignedDocument { pub fn into_builder(&self) -> anyhow::Result { self.try_into() } + + /// Returns CBOR bytes. + /// + /// # Errors + /// - `minicbor::encode::Error` + pub fn to_bytes(&self) -> anyhow::Result> { + let mut e = minicbor::Encoder::new(Vec::new()); + self.encode(&mut e, &mut ())?; + Ok(e.into_writer()) + } + + /// Build `CatalystSignedDoc` instance from CBOR bytes. + /// + /// # Errors + /// - `minicbor::decode::Error` + pub fn from_bytes( + bytes: &[u8], + mut policy: CompatibilityPolicy, + ) -> anyhow::Result { + Ok(minicbor::decode_with(bytes, &mut policy)?) + } } impl Decode<'_, CompatibilityPolicy> for CatalystSignedDocument { @@ -344,17 +365,14 @@ impl TryFrom<&[u8]> for CatalystSignedDocument { type Error = anyhow::Error; fn try_from(value: &[u8]) -> Result { - Ok(minicbor::decode_with( - value, - &mut CompatibilityPolicy::Accept, - )?) + Self::from_bytes(value, CompatibilityPolicy::Accept) } } -impl TryFrom for Vec { +impl TryFrom<&CatalystSignedDocument> for Vec { type Error = anyhow::Error; - fn try_from(value: CatalystSignedDocument) -> Result { - Ok(minicbor::to_vec(value)?) + fn try_from(value: &CatalystSignedDocument) -> Result { + value.to_bytes() } } diff --git a/rust/signed_doc/src/metadata/collaborators.rs b/rust/signed_doc/src/metadata/collaborators.rs index ababb3518e2..2cfa1df5c57 100644 --- a/rust/signed_doc/src/metadata/collaborators.rs +++ b/rust/signed_doc/src/metadata/collaborators.rs @@ -29,16 +29,14 @@ impl minicbor::Encode<()> for Collaborators { e: &mut minicbor::Encoder, _ctx: &mut (), ) -> Result<(), minicbor::encode::Error> { - if !self.0.is_empty() { - e.array( - self.0 - .len() - .try_into() - .map_err(minicbor::encode::Error::message)?, - )?; - for c in &self.0 { - e.bytes(&c.to_string().into_bytes())?; - } + e.array( + self.0 + .len() + .try_into() + .map_err(minicbor::encode::Error::message)?, + )?; + for c in &self.0 { + e.bytes(&c.to_string().into_bytes())?; } Ok(()) } @@ -49,15 +47,35 @@ impl minicbor::Decode<'_, ()> for Collaborators { d: &mut minicbor::Decoder<'_>, _ctx: &mut (), ) -> Result { - Array::decode(d, &mut DecodeCtx::Deterministic)? + Array::decode(d, &mut DecodeCtx::Deterministic) + .and_then(|arr| { + if arr.is_empty() { + Err(minicbor::decode::Error::message( + "collaborators array must have at least one element", + )) + } else { + Ok(arr) + } + })? .iter() .map(|item| minicbor::Decoder::new(item).bytes()) .collect::, _>>()? .into_iter() - .map(CatalystId::try_from) + .map(|id| { + CatalystId::try_from(id) + .map_err(minicbor::decode::Error::custom) + .and_then(|id| { + if id.is_uri() { + Ok(id) + } else { + Err(minicbor::decode::Error::message(format!( + "provided CatalystId {id} must in URI format for collaborators field" + ))) + } + }) + }) .collect::>() .map(Self) - .map_err(minicbor::decode::Error::custom) } } @@ -66,10 +84,21 @@ impl<'de> serde::Deserialize<'de> for Collaborators { where D: serde::Deserializer<'de> { Vec::::deserialize(deserializer)? .into_iter() - .map(|id| CatalystId::from_str(&id)) + .map(|id| { + CatalystId::from_str(&id) + .map_err(serde::de::Error::custom) + .and_then(|id| { + if id.is_uri() { + Ok(id) + } else { + Err(serde::de::Error::custom(format!( + "provided CatalystId {id} must in ID format for collaborators field" + ))) + } + }) + }) .collect::>() .map(Self) - .map_err(serde::de::Error::custom) } } @@ -85,3 +114,42 @@ impl serde::Serialize for Collaborators { serializer.collect_seq(iter) } } + +#[cfg(test)] +mod tests { + use minicbor::{Decode, Decoder, Encoder}; + use test_case::test_case; + + use super::*; + + #[test_case( + { + Encoder::new(Vec::new()) + } ; + "Invalid empty CBOR bytes" + )] + #[test_case( + { + let mut e = Encoder::new(Vec::new()); + e.array(0).unwrap(); + e + } ; + "Empty CBOR array" + )] + #[test_case( + { + let mut e = Encoder::new(Vec::new()); + e.array(1).unwrap(); + /* cspell:disable */ + e.bytes(b"preprod.cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE/7/3").unwrap(); + /* cspell:enable */ + e + } ; + "CatalystId not in ID form" + )] + fn test_invalid_cbor_decode(e: Encoder>) { + assert!( + Collaborators::decode(&mut Decoder::new(e.into_writer().as_slice()), &mut ()).is_err() + ); + } +} diff --git a/rust/signed_doc/src/validator/rules/ownership/mod.rs b/rust/signed_doc/src/validator/rules/ownership/mod.rs index 88bf83e3493..6755e767308 100644 --- a/rust/signed_doc/src/validator/rules/ownership/mod.rs +++ b/rust/signed_doc/src/validator/rules/ownership/mod.rs @@ -14,18 +14,6 @@ use crate::{providers::CatalystSignedDocumentProvider, CatalystSignedDocument}; /// Context for the validation problem report. const REPORT_CONTEXT: &str = "Document ownership validation"; -/// Returns `true` if the document has a single author. -/// -/// If not, it adds to the document's problem report. -fn single_author_check(doc: &CatalystSignedDocument) -> bool { - let is_valid = doc.authors().len() == 1; - if !is_valid { - doc.report() - .functional_validation("Document must only be signed by one author", REPORT_CONTEXT); - } - is_valid -} - /// Document Ownership Validation Rule #[derive(Debug)] pub(crate) struct DocumentOwnershipRule { @@ -53,10 +41,17 @@ impl DocumentOwnershipRule { Provider: CatalystSignedDocumentProvider, { let doc_id = doc.doc_id()?; - let first_doc_opt = provider.try_get_first_doc(doc_id).await?; - if self.allow_collaborators { - if let Some(first_doc) = first_doc_opt { + if doc_id == doc.doc_ver()? && doc.authors().len() != 1 { + doc.report().functional_validation( + "Document must only be signed by one author", + REPORT_CONTEXT, + ); + return Ok(false); + } + + if let Some(first_doc) = provider.try_get_first_doc(doc_id).await? { + if self.allow_collaborators { // This a new version of an existing `doc_id` let Some(last_doc) = provider.try_get_last_doc(doc_id).await? else { anyhow::bail!( @@ -72,40 +67,34 @@ impl DocumentOwnershipRule { let mut allowed_authors = first_doc .authors() .into_iter() - .map(CatalystId::as_uri) .collect::>(); allowed_authors.extend( last_doc .doc_meta() .collaborators() .iter() - .cloned() - .map(CatalystId::as_uri), + .map(CatalystId::as_short_id), ); - let doc_authors = doc - .authors() - .into_iter() - .map(CatalystId::as_uri) - .collect::>(); + let doc_authors = doc.authors().into_iter().collect::>(); - let is_valid = allowed_authors.intersection(&doc_authors).count() > 0; + // all elements of the `doc_authors` should be intersecting with the + // `allowed_authors` + let is_valid = + allowed_authors.intersection(&doc_authors).count() == doc_authors.len(); if !is_valid { doc.report().functional_validation( - "Document must only be signed by original author and/or by collaborators defined in the previous version", + &format!( + "Document must only be signed by original author and/or by collaborators defined in the previous version. Allowed signers: {:?}, Document signers: {:?}", + allowed_authors.iter().map(ToString::to_string).collect::>(), + doc_authors.iter().map(ToString::to_string).collect::>() + ), REPORT_CONTEXT, ); } return Ok(is_valid); } - - // This is a first version of the doc - return Ok(single_author_check(doc)); - } - - // No collaborators are allowed - if let Some(first_doc) = first_doc_opt { - // This a new version of an existing `doc_id` + // No collaborators are allowed let is_valid = first_doc.authors() == doc.authors(); if !is_valid { doc.report().functional_validation( @@ -116,7 +105,6 @@ impl DocumentOwnershipRule { return Ok(is_valid); } - // This is a first version of the doc - Ok(single_author_check(doc)) + Ok(true) } } diff --git a/rust/signed_doc/src/validator/rules/ownership/tests.rs b/rust/signed_doc/src/validator/rules/ownership/tests.rs index 1c0ffc0c0c9..f26ed943403 100644 --- a/rust/signed_doc/src/validator/rules/ownership/tests.rs +++ b/rust/signed_doc/src/validator/rules/ownership/tests.rs @@ -1,198 +1,217 @@ //! Ownership Validation Rule testing -use catalyst_types::{ - catalyst_id::{role_index::RoleId, CatalystId}, - uuid::{UuidV4, UuidV7}, -}; +use catalyst_types::{catalyst_id::role_index::RoleId, uuid::UuidV7}; use ed25519_dalek::ed25519::signature::Signer; -use rand::{thread_rng, Rng}; use test_case::test_case; use super::*; use crate::{ builder::tests::Builder, metadata::SupportedField, providers::tests::TestCatalystProvider, - validator::rules::utils::create_dummy_key_pair, ContentType, + validator::rules::utils::create_dummy_key_pair, }; -const NO_AUTHOR: usize = 0; -const ONE_AUTHOR: usize = 1; - -const NO_COLLABORATORS: usize = 0; -const THREE_COLLABORATORS: usize = 3; - -#[derive(Clone)] -struct CatalystAuthorId { - sk: ed25519_dalek::SigningKey, - kid: CatalystId, -} - -type CatDoc = CatalystSignedDocument; - -type DocId = UuidV7; - -type Authors = Vec; - -type Collaborators = Vec; - -impl CatalystAuthorId { - fn new() -> Self { - let (sk, _, kid) = create_dummy_key_pair(RoleId::Role0); - Self { sk, kid } - } -} - -fn doc_builder( - doc_id: UuidV7, - doc_ver: UuidV7, - authors: Authors, - collaborators: Collaborators, -) -> (UuidV7, Authors, CatalystSignedDocument) { - let mut doc_builder = Builder::new() - .with_metadata_field(SupportedField::Id(doc_id)) - .with_metadata_field(SupportedField::Ver(doc_ver)) - .with_metadata_field(SupportedField::Type(UuidV4::new().into())) - .with_metadata_field(SupportedField::ContentType(ContentType::Json)); - - if !collaborators.is_empty() { - let collaborators = collaborators - .into_iter() - .map(|c| c.kid) - .collect::>(); - doc_builder = - doc_builder.with_metadata_field(SupportedField::Collaborators(collaborators.into())); - } - - for author in &authors { - doc_builder = doc_builder - .add_signature(|m| author.sk.sign(&m).to_vec(), author.kid.clone()) - .unwrap(); - } - (doc_id, authors, doc_builder.build()) -} - -fn gen_authors(size_of: usize) -> Authors { - (0..size_of).map(|_| CatalystAuthorId::new()).collect() -} - -fn gen_next_ver_doc( - doc_id: UuidV7, - authors: Authors, - collaborators: Collaborators, -) -> CatalystSignedDocument { - let (_, _, new_doc) = doc_builder(doc_id, UuidV7::new(), authors, collaborators); - new_doc -} - -fn gen_original_doc_and_provider( - num_authors: usize, - num_collaborators: usize, -) -> (CatDoc, DocId, Authors, Collaborators) { - let authors = gen_authors(num_authors); - let collaborators = gen_authors(num_collaborators); - let doc_id = UuidV7::new(); - let doc_ver_1 = UuidV7::new(); - let (_, _, doc_1) = doc_builder(doc_id, doc_ver_1, authors.clone(), collaborators.clone()); - (doc_1, doc_id, authors, collaborators) -} - #[test_case( - |_provider| { - let (doc_1, _, _, _) = gen_original_doc_and_provider(ONE_AUTHOR,NO_COLLABORATORS); - doc_1 - } => true ; + |_| { + let (sk, _, kid) = create_dummy_key_pair(RoleId::Role0); + let id = UuidV7::new(); + Builder::new() + .with_metadata_field(SupportedField::Id(id)) + .with_metadata_field(SupportedField::Ver(id)) + .add_signature(|m| sk.sign(&m).to_vec(), kid.clone()) + .unwrap() + .build() + } => (true, true) ; "First Version Catalyst Signed Document has only one author" )] #[test_case( |provider| { - let (doc_1, doc_id, authors, _) = gen_original_doc_and_provider(ONE_AUTHOR,NO_COLLABORATORS); - provider.add_document(None, &doc_1).unwrap(); - gen_next_ver_doc(doc_id, authors, Vec::new()) - } => true ; - "Latest Version Catalyst Signed Document has the same author as the first version" + let (sk, _, kid) = create_dummy_key_pair(RoleId::Role0); + let id = UuidV7::new(); + let doc = Builder::new() + .with_metadata_field(SupportedField::Id(id)) + .with_metadata_field(SupportedField::Ver(id)) + .add_signature(|m| sk.sign(&m).to_vec(), kid.clone()) + .unwrap() + .build(); + provider.add_document(None, &doc).unwrap(); + + Builder::new() + .with_metadata_field(SupportedField::Id(id)) + .with_metadata_field(SupportedField::Ver(UuidV7::new())) + .add_signature(|m| sk.sign(&m).to_vec(), kid.clone()) + .unwrap() + .build() + } => (true, true) ; + "Latest Version Catalyst Signed Document has the same author as the first version" )] #[test_case( - |_provider| { - let (doc_1, _doc_id, _authors, _) = gen_original_doc_and_provider(NO_AUTHOR,NO_COLLABORATORS); - doc_1 - } => false ; - "First Version Unsigned Catalyst Document fails" + |provider| { + let (a_sk, _, a_kid) = create_dummy_key_pair(RoleId::Role0); + let (c_sk, _, c_kid) = create_dummy_key_pair(RoleId::Role0); + let id = UuidV7::new(); + let doc = Builder::new() + .with_metadata_field(SupportedField::Id(id)) + .with_metadata_field(SupportedField::Ver(id)) + .with_metadata_field(SupportedField::Collaborators(vec![c_kid.clone()].into())) + .add_signature(|m| a_sk.sign(&m).to_vec(), a_kid.clone()) + .unwrap() + .build(); + provider.add_document(None, &doc).unwrap(); + + Builder::new() + .with_metadata_field(SupportedField::Id(id)) + .with_metadata_field(SupportedField::Ver(UuidV7::new())) + .add_signature(|m| a_sk.sign(&m).to_vec(), a_kid.clone()) + .unwrap() + .add_signature(|m| c_sk.sign(&m).to_vec(), c_kid.clone()) + .unwrap() + .build() + } => (false, true) ; + "Latest Version Catalyst Signed Document signed by first author and one collaborator" )] #[test_case( |provider| { - let (doc_1, doc_id, _authors, _) = gen_original_doc_and_provider(ONE_AUTHOR,NO_COLLABORATORS); - provider.add_document(None, &doc_1).unwrap(); - let other_author = gen_authors(ONE_AUTHOR); - gen_next_ver_doc(doc_id, other_author, Vec::new()) - } => false ; - "Latest Catalyst Signed Document has a different author from the first version" + let (a_sk, _, a_kid) = create_dummy_key_pair(RoleId::Role0); + let (c_sk, _, c_kid) = create_dummy_key_pair(RoleId::Role0); + let id = UuidV7::new(); + let doc = Builder::new() + .with_metadata_field(SupportedField::Id(id)) + .with_metadata_field(SupportedField::Ver(id)) + .with_metadata_field(SupportedField::Collaborators(vec![c_kid.clone()].into())) + .add_signature(|m| a_sk.sign(&m).to_vec(), a_kid.clone()) + .unwrap() + .build(); + provider.add_document(None, &doc).unwrap(); + + Builder::new() + .with_metadata_field(SupportedField::Id(id)) + .with_metadata_field(SupportedField::Ver(UuidV7::new())) + .add_signature(|m| c_sk.sign(&m).to_vec(), c_kid.clone()) + .unwrap() + .build() + } => (false, true) ; + "Latest Version Catalyst Signed Document signed by one collaborator" )] -#[tokio::test] -async fn simple_author_rule_test( - test_case_fn: impl FnOnce(&mut TestCatalystProvider) -> CatalystSignedDocument -) -> bool { - let rule = DocumentOwnershipRule { - allow_collaborators: false, - }; - - let mut provider = TestCatalystProvider::default(); - let doc = test_case_fn(&mut provider); - - rule.check(&doc, &provider).await.unwrap() -} - #[test_case( - |_provider| { - let (doc_1, _, _, _) = gen_original_doc_and_provider(ONE_AUTHOR,NO_COLLABORATORS); - doc_1 - } => true ; - "First Version Catalyst Signed Document has the only one author" + |_| { + let id = UuidV7::new(); + Builder::new() + .with_metadata_field(SupportedField::Id(id)) + .with_metadata_field(SupportedField::Ver(id)) + .build() + } => (false, false) ; + "First Version Unsigned Catalyst Document" )] #[test_case( - |provider| { - let (doc_1, doc_id, mut authors, collaborators) = gen_original_doc_and_provider(ONE_AUTHOR,THREE_COLLABORATORS); - provider.add_document(None, &doc_1).unwrap(); - authors.extend_from_slice(&collaborators); - gen_next_ver_doc(doc_id, authors, Vec::new()) - } => true ; - "Latest Version Catalyst Signed Document signed by first author and all collaborators" + |_| { + let (sk1, _, kid1) = create_dummy_key_pair(RoleId::Role0); + let (sk2, _, kid2) = create_dummy_key_pair(RoleId::Role0); + let id = UuidV7::new(); + Builder::new() + .with_metadata_field(SupportedField::Id(id)) + .with_metadata_field(SupportedField::Ver(id)) + .add_signature(|m| sk1.sign(&m).to_vec(), kid1.clone()) + .unwrap() + .add_signature(|m| sk2.sign(&m).to_vec(), kid2.clone()) + .unwrap() + .build() + } => (false, false) ; + "First Version Catalyst Signed Document two authors" )] -#[allow(clippy::indexing_slicing)] #[test_case( |provider| { - let (doc_1, doc_id, _, collaborators) = gen_original_doc_and_provider(ONE_AUTHOR,THREE_COLLABORATORS); - provider.add_document(None, &doc_1).unwrap(); + let (sk, _, kid) = create_dummy_key_pair(RoleId::Role0); + let id = UuidV7::new(); + let doc = Builder::new() + .with_metadata_field(SupportedField::Id(id)) + .with_metadata_field(SupportedField::Ver(id)) + .add_signature(|m| sk.sign(&m).to_vec(), kid.clone()) + .unwrap() + .build(); + provider.add_document(None, &doc).unwrap(); - let random_collaborator = collaborators[thread_rng().gen_range(0..THREE_COLLABORATORS)].clone(); - gen_next_ver_doc(doc_id, vec![random_collaborator], Vec::new()) - } => true ; - "Latest Version Catalyst Signed Document signed by collaborator" + let (sk, _, kid) = create_dummy_key_pair(RoleId::Role0); + Builder::new() + .with_metadata_field(SupportedField::Id(id)) + .with_metadata_field(SupportedField::Ver(UuidV7::new())) + .add_signature(|m| sk.sign(&m).to_vec(), kid.clone()) + .unwrap() + .build() + } => (false, false) ; + "Latest Catalyst Signed Document has a different author from the first version" )] #[test_case( - |_provider| { - let (doc_1, _doc_id, _authors, _) = gen_original_doc_and_provider(NO_AUTHOR,NO_COLLABORATORS); - doc_1 - } => false ; - "First Version Unsigned Catalyst Document fails" + |provider| { + let (a_sk, _, a_kid) = create_dummy_key_pair(RoleId::Role0); + let (c_sk, _, c_kid) = create_dummy_key_pair(RoleId::Role0); + let id = UuidV7::new(); + let doc = Builder::new() + .with_metadata_field(SupportedField::Id(id)) + .with_metadata_field(SupportedField::Ver(id)) + .add_signature(|m| a_sk.sign(&m).to_vec(), a_kid.clone()) + .unwrap() + .build(); + provider.add_document(None, &doc).unwrap(); + + Builder::new() + .with_metadata_field(SupportedField::Id(id)) + .with_metadata_field(SupportedField::Ver(UuidV7::new())) + .add_signature(|m| a_sk.sign(&m).to_vec(), a_kid.clone()) + .unwrap() + .add_signature(|m| c_sk.sign(&m).to_vec(), c_kid.clone()) + .unwrap() + .build() + } => (false, false) ; + "Latest Version Catalyst Signed Document signed by first author and not added collaborator" )] #[test_case( |provider| { - let (doc_1, doc_id, _authors, collaborators) = gen_original_doc_and_provider(ONE_AUTHOR,THREE_COLLABORATORS); - provider.add_document(None, &doc_1).unwrap(); - let other_authors = gen_authors(ONE_AUTHOR); - gen_next_ver_doc(doc_id, other_authors, collaborators) - } => false ; - "Latest Catalyst Signed Document signed by unexpected author" + let (a_sk, _, a_kid) = create_dummy_key_pair(RoleId::Role0); + let (c1_sk, _, c1_kid) = create_dummy_key_pair(RoleId::Role0); + let id = UuidV7::new(); + let doc = Builder::new() + .with_metadata_field(SupportedField::Id(id)) + .with_metadata_field(SupportedField::Ver(id)) + .with_metadata_field(SupportedField::Collaborators(vec![c1_kid.clone()].into())) + .add_signature(|m| a_sk.sign(&m).to_vec(), a_kid.clone()) + .unwrap() + .build(); + provider.add_document(None, &doc).unwrap(); + + let (c2_sk, _, c2_kid) = create_dummy_key_pair(RoleId::Role0); + Builder::new() + .with_metadata_field(SupportedField::Id(id)) + .with_metadata_field(SupportedField::Ver(UuidV7::new())) + .add_signature(|m| a_sk.sign(&m).to_vec(), a_kid.clone()) + .unwrap() + .add_signature(|m| c1_sk.sign(&m).to_vec(), c1_kid.clone()) + .unwrap() + .add_signature(|m| c2_sk.sign(&m).to_vec(), c2_kid.clone()) + .unwrap() + .build() + } => (false, false) ; + "Latest Version Catalyst Signed Document signed by first author and one collaborator and one unknown collaborator" )] #[tokio::test] -async fn author_with_collaborators_rule_test( - test_case_fn: impl FnOnce(&mut TestCatalystProvider) -> CatalystSignedDocument -) -> bool { - let rule = DocumentOwnershipRule { - allow_collaborators: true, - }; - +async fn ownership_without_collaborators_test( + doc_gen: impl FnOnce(&mut TestCatalystProvider) -> CatalystSignedDocument +) -> (bool, bool) { let mut provider = TestCatalystProvider::default(); - let doc = test_case_fn(&mut provider); - rule.check(&doc, &provider).await.unwrap() + let doc = doc_gen(&mut provider); + + let without_collaborators = DocumentOwnershipRule { + allow_collaborators: false, + } + .check(&doc, &provider) + .await + .unwrap(); + let allowed_collaborators = DocumentOwnershipRule { + allow_collaborators: true, + } + .check(&doc, &provider) + .await + .unwrap(); + println!("{:?}", doc.report()); + (without_collaborators, allowed_collaborators) } diff --git a/rust/signed_doc/src/validator/rules/signature/mod.rs b/rust/signed_doc/src/validator/rules/signature/mod.rs index 1bfedc4f47c..193f2c9f750 100644 --- a/rust/signed_doc/src/validator/rules/signature/mod.rs +++ b/rust/signed_doc/src/validator/rules/signature/mod.rs @@ -1,5 +1,8 @@ //! Validator for Signatures +#[cfg(test)] +mod tests; + use anyhow::Context; use catalyst_types::problem_report::ProblemReport; @@ -109,306 +112,3 @@ where 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::*, validator::rules::utils::create_dummy_key_pair, *}; - - 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) = create_dummy_key_pair(RoleId::Role0); - - 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, ..) = create_dummy_key_pair(RoleId::Role0); - 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) = create_dummy_key_pair(RoleId::Role0); - let (sk2, pk2, kid2) = create_dummy_key_pair(RoleId::Role0); - let (sk3, pk3, kid3) = create_dummy_key_pair(RoleId::Role0); - let (_, pk_n, kid_n) = create_dummy_key_pair(RoleId::Role0); - - 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) = create_dummy_key_pair(RoleId::Role0); - 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/signature/tests.rs b/rust/signed_doc/src/validator/rules/signature/tests.rs index 80fd1d3a0df..70db477941a 100644 --- a/rust/signed_doc/src/validator/rules/signature/tests.rs +++ b/rust/signed_doc/src/validator/rules/signature/tests.rs @@ -19,7 +19,7 @@ fn metadata() -> serde_json::Value { "section": "$", "collaborators": vec![ /* cspell:disable */ - "cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE", + "id.catalyst://cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE", "id.catalyst://preprod.cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE/7/3" /* cspell:enable */ ], diff --git a/rust/signed_doc/src/validator/rules/signature_kid.rs b/rust/signed_doc/src/validator/rules/signature_kid.rs index c9b6b1521cb..fa0d2a58398 100644 --- a/rust/signed_doc/src/validator/rules/signature_kid.rs +++ b/rust/signed_doc/src/validator/rules/signature_kid.rs @@ -11,7 +11,7 @@ use crate::CatalystSignedDocument; #[derive(Debug)] pub(crate) struct SignatureKidRule { /// expected `RoleId` values for the `kid` field - pub(crate) allowed_roles: HashSet, + allowed_roles: HashSet, } impl SignatureKidRule { diff --git a/rust/signed_doc/src/validator/rules/utils.rs b/rust/signed_doc/src/validator/rules/utils.rs index d876d1b550b..9db46bf89c7 100644 --- a/rust/signed_doc/src/validator/rules/utils.rs +++ b/rust/signed_doc/src/validator/rules/utils.rs @@ -60,8 +60,9 @@ pub(super) fn create_dummy_key_pair( ) { let sk = create_signing_key(); let pk = sk.verifying_key(); - let kid = - catalyst_types::catalyst_id::CatalystId::new("cardano", None, pk).with_role(role_index); + let kid = catalyst_types::catalyst_id::CatalystId::new("cardano", None, pk) + .with_role(role_index) + .as_uri(); (sk, pk, kid) } diff --git a/rust/signed_doc/tests/decoding.rs b/rust/signed_doc/tests/decoding.rs index ad2d40ac22a..aaa21ef5ab5 100644 --- a/rust/signed_doc/tests/decoding.rs +++ b/rust/signed_doc/tests/decoding.rs @@ -632,7 +632,7 @@ fn signed_doc_with_complete_metadata_fields_case() -> TestCase { /* cspell:disable */ p_headers.str("collaborators")?; p_headers.array(2)?; - p_headers.bytes(b"cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE")?; + p_headers.bytes(b"id.catalyst://cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE")?; p_headers.bytes(b"id.catalyst://preprod.cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE/7/3")?; /* cspell:enable */ p_headers.str("parameters")?.encode_with(uuid_v7, &mut catalyst_types::uuid::CborContext::Tagged)?; diff --git a/specs/definitions/signed_docs/authors_copyright.cue b/specs/definitions/signed_docs/authors_copyright.cue index 25bfda36b1a..87cb8c4a024 100644 --- a/specs/definitions/signed_docs/authors_copyright.cue +++ b/specs/definitions/signed_docs/authors_copyright.cue @@ -116,5 +116,12 @@ copyright: #copyrightNotice & { * Fixed an invalid 'Presentation Template' JSON schema. """ }, + { + version: "0.1.4" + modified: "2025-10-17" + changes: """ + * Modified `collaborators` cddl definition, it must have at least one element in array. + """ + }, ] } diff --git a/specs/definitions/signed_docs/cddl_defs.cue b/specs/definitions/signed_docs/cddl_defs.cue index 014872fb442..8190a5ce56e 100644 --- a/specs/definitions/signed_docs/cddl_defs.cue +++ b/specs/definitions/signed_docs/cddl_defs.cue @@ -109,7 +109,7 @@ cddlDefinitions: #cddlDefinitions & { comment: "Reference to a section in a referenced document." } collaborators: { - def: "[ * \(requires[0]) ]" + def: "[ + \(requires[0]) ]" requires: ["catalyst_id_kid"] comment: "Allowed Collaborators on the next subsequent version of a document." } diff --git a/specs/signed_doc.json b/specs/signed_doc.json index 5c80b8dc3f2..4a0869e5a4d 100644 --- a/specs/signed_doc.json +++ b/specs/signed_doc.json @@ -121,7 +121,7 @@ }, "collaborators": { "comment": "Allowed Collaborators on the next subsequent version of a document.", - "def": "[ * catalyst_id_kid ]", + "def": "[ + catalyst_id_kid ]", "requires": [ "catalyst_id_kid" ] @@ -314,6 +314,11 @@ "changes": "* Fixed an invalid 'Presentation Template' JSON schema. ", "modified": "2025-09-09", "version": "0.1.3" + }, + { + "changes": "* Modified `collaborators` cddl definition, it must have at least one element in array.", + "modified": "2025-10-17", + "version": "0.1.4" } ] },