From 4e5ff19aacfd271f6b13b9d37f661fbd70da587c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Rosales?= Date: Mon, 27 Jan 2025 12:25:08 -0600 Subject: [PATCH 01/17] fix(rust/signed-doc): add content type validation --- rust/signed_doc/src/content.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/rust/signed_doc/src/content.rs b/rust/signed_doc/src/content.rs index 9b49fc53f1..00618ac9eb 100644 --- a/rust/signed_doc/src/content.rs +++ b/rust/signed_doc/src/content.rs @@ -41,11 +41,22 @@ impl Content { /// /// # Errors /// Returns an error if content is not correctly encoded - #[allow(clippy::unnecessary_wraps)] pub(crate) fn from_decoded( data: Vec, content_type: ContentType, content_encoding: Option, ) -> anyhow::Result { // TODO add content_type verification + match content_type { + ContentType::Json => { + if let Err(e) = serde_json::from_slice::(&data) { + anyhow::bail!("Invalid {content_type} content: {e}") + } + }, + ContentType::Cbor => { + if let Err(e) = minicbor::decode::(&data) { + anyhow::bail!("Invalid {content_type} content: {e}") + } + }, + } Ok(Self { data, content_type, From 15741c6bfccb80696c83f8a2a48884817f2ae195 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Rosales?= Date: Mon, 27 Jan 2025 12:42:17 -0600 Subject: [PATCH 02/17] fix(rust/signed-doc): content encoding field is optional --- rust/signed_doc/src/metadata/mod.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/rust/signed_doc/src/metadata/mod.rs b/rust/signed_doc/src/metadata/mod.rs index 5722e99ba8..6119368113 100644 --- a/rust/signed_doc/src/metadata/mod.rs +++ b/rust/signed_doc/src/metadata/mod.rs @@ -156,11 +156,6 @@ impl Metadata { ); }, } - } else { - error_report.missing_field( - "content encoding", - "Missing content encoding field in COSE protected header", - ); } let mut doc_type: Option = None; From 3d34e470457cd7a7e4c4ba6a23e6551141849212 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Rosales?= Date: Mon, 27 Jan 2025 12:43:14 -0600 Subject: [PATCH 03/17] fix(rust/signed-doc): add 'half' feature from minicbor crate --- rust/signed_doc/Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rust/signed_doc/Cargo.toml b/rust/signed_doc/Cargo.toml index c4743942c3..f44c177934 100644 --- a/rust/signed_doc/Cargo.toml +++ b/rust/signed_doc/Cargo.toml @@ -16,7 +16,7 @@ anyhow = "1.0.95" serde = { version = "1.0.217", features = ["derive"] } serde_json = "1.0.134" coset = "0.3.8" -minicbor = "0.25.1" +minicbor = { version = "0.25.1", features = ["half"] } brotli = "7.0.0" ed25519-dalek = { version = "2.1.1", features = ["pem", "rand_core"] } uuid = { version = "1.11.0", features = ["v4", "v7", "serde"] } @@ -31,4 +31,4 @@ rand = "0.8.5" [[bin]] name = "signed-docs" -path = "examples/mk_signed_doc.rs" \ No newline at end of file +path = "examples/mk_signed_doc.rs" From 3a66559f7dccc6e561ea3dd9fb37575a6fc3ade4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Rosales?= Date: Mon, 27 Jan 2025 18:26:35 -0600 Subject: [PATCH 04/17] wip(rust/signed_doc): implement verification method * verify signatures with public (verification) key * todo: verify UUIDs --- rust/signed_doc/src/lib.rs | 67 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/rust/signed_doc/src/lib.rs b/rust/signed_doc/src/lib.rs index 24cc5d10dc..6cb593effd 100644 --- a/rust/signed_doc/src/lib.rs +++ b/rust/signed_doc/src/lib.rs @@ -17,6 +17,7 @@ pub use builder::Builder; use catalyst_types::problem_report::ProblemReport; pub use content::Content; use coset::{CborSerializable, Header}; +use ed25519_dalek::VerifyingKey; use error::CatalystSignedDocError; pub use metadata::{DocumentRef, ExtraFields, Metadata, UuidV4, UuidV7}; pub use minicbor::{decode, encode, Decode, Decoder, Encode}; @@ -105,6 +106,72 @@ impl CatalystSignedDocument { pub fn signatures(&self) -> &Signatures { &self.inner.signatures } + + /// Verify document signatures and `UUID`s. + /// + /// # Errors + /// + /// Returns a report of verification failures and the source error. + #[allow(clippy::indexing_slicing)] + pub fn verify

(&self, pk_getter: P) -> Result<(), CatalystSignedDocError> + where P: Fn(&KidUri) -> VerifyingKey { + let error_report = ProblemReport::new("Catalyst Signed Document Verification"); + + match self.as_cose_sign() { + Ok(cose_sign) => { + let signatures = self.signatures().cose_signatures(); + for (idx, kid) in self.signatures().kids().iter().enumerate() { + let pk = pk_getter(kid); + let signature = &signatures[idx]; + let tbs_data = cose_sign.tbs_data(&[], signature); + match signature.signature.as_slice().try_into() { + Ok(signature_bytes) => { + let signature = ed25519_dalek::Signature::from_bytes(signature_bytes); + if let Err(e) = pk.verify_strict(&tbs_data, &signature) { + error_report.functional_validation( + &format!( + "Verification failed for signature with Key ID {kid}: {e}" + ), + "During signature validation with verifying key", + ); + } + }, + Err(_) => { + error_report.invalid_value( + "cose signature", + &format!("{}", signature.signature.len()), + &format!("must be {}", ed25519_dalek::Signature::BYTE_SIZE), + "During encoding cose signature to bytes", + ); + }, + } + } + }, + Err(e) => { + error_report.other( + &format!("{e}"), + "During encoding signed document as COSE SIGN", + ); + }, + } + + if error_report.is_problematic() { + return Err(CatalystSignedDocError::new( + error_report, + anyhow::anyhow!("Verification failed for Catalyst Signed Document"), + )); + } + + Ok(()) + } + + /// Convert Catalyst Signed Document into `coset::CoseSign` + fn as_cose_sign(&self) -> anyhow::Result { + let mut cose_bytes: Vec = Vec::new(); + minicbor::encode(self, &mut cose_bytes)?; + coset::CoseSign::from_slice(&cose_bytes) + .map_err(|e| anyhow::anyhow!("encoding COSE SIGN failed: {e}")) + } } impl TryFrom<&[u8]> for CatalystSignedDocument { From 70440b5b7caf579ce14451623cc4463474dc981f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Rosales?= Date: Mon, 27 Jan 2025 19:16:14 -0600 Subject: [PATCH 05/17] fix(rust/signed-doc): fix content-type verification --- rust/signed_doc/src/content.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/rust/signed_doc/src/content.rs b/rust/signed_doc/src/content.rs index 00618ac9eb..2bfb4a9781 100644 --- a/rust/signed_doc/src/content.rs +++ b/rust/signed_doc/src/content.rs @@ -44,10 +44,9 @@ impl Content { pub(crate) fn from_decoded( data: Vec, content_type: ContentType, content_encoding: Option, ) -> anyhow::Result { - // TODO add content_type verification match content_type { ContentType::Json => { - if let Err(e) = serde_json::from_slice::(&data) { + if let Err(e) = serde_json::to_value(&data) { anyhow::bail!("Invalid {content_type} content: {e}") } }, From bd4561d6d2a571b9b83e34312c9db114ae417d45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Rosales?= Date: Mon, 27 Jan 2025 20:21:02 -0600 Subject: [PATCH 06/17] wip(rust/signed_doc): implement verification method * verify doc type, id and ver --- rust/signed_doc/src/lib.rs | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/rust/signed_doc/src/lib.rs b/rust/signed_doc/src/lib.rs index 6cb593effd..44297b08e7 100644 --- a/rust/signed_doc/src/lib.rs +++ b/rust/signed_doc/src/lib.rs @@ -117,6 +117,36 @@ impl CatalystSignedDocument { where P: Fn(&KidUri) -> VerifyingKey { let error_report = ProblemReport::new("Catalyst Signed Document Verification"); + if !self.doc_type().is_valid() { + error_report.functional_validation( + &format!("{} is invalid UUIDv4", self.doc_type()), + "During Document Type UUID verification", + ); + } + + let doc_id = self.doc_id(); + if !doc_id.is_valid() { + error_report.functional_validation( + &format!("{doc_id} is invalid UUIDv7"), + "During Document ID UUID verification", + ); + } + + let doc_ver = self.doc_ver(); + if !doc_ver.is_valid() { + error_report.functional_validation( + &format!("{doc_ver} is invalid UUIDv7"), + "During Document Version UUID verification", + ); + } + + if doc_id.is_valid() && doc_ver.is_valid() && doc_ver < doc_id { + error_report.functional_validation( + &format!("Document Version {doc_ver} is smaller than Document Id {doc_id}"), + "During Document Version UUID verification", + ); + } + match self.as_cose_sign() { Ok(cose_sign) => { let signatures = self.signatures().cose_signatures(); From 2392ed5ca2b144bf04847e2afe76c99c8d3d416d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Rosales?= Date: Mon, 27 Jan 2025 20:30:07 -0600 Subject: [PATCH 07/17] wip(rust/signed_doc): implement verification method * verify extra fields --- rust/signed_doc/src/lib.rs | 66 +++++++++++++++++++- rust/signed_doc/src/metadata/document_ref.rs | 11 ++++ 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/rust/signed_doc/src/lib.rs b/rust/signed_doc/src/lib.rs index 44297b08e7..fd286d63c2 100644 --- a/rust/signed_doc/src/lib.rs +++ b/rust/signed_doc/src/lib.rs @@ -112,7 +112,7 @@ impl CatalystSignedDocument { /// # Errors /// /// Returns a report of verification failures and the source error. - #[allow(clippy::indexing_slicing)] + #[allow(clippy::indexing_slicing, clippy::too_many_lines)] pub fn verify

(&self, pk_getter: P) -> Result<(), CatalystSignedDocError> where P: Fn(&KidUri) -> VerifyingKey { let error_report = ProblemReport::new("Catalyst Signed Document Verification"); @@ -147,6 +147,70 @@ impl CatalystSignedDocument { ); } + let extra = self.doc_meta(); + if let Some(doc_ref) = extra.doc_ref() { + if !doc_ref.is_valid() { + error_report.functional_validation( + &format!("Document Reference {doc_ref:?} is invalid"), + "During Document Reference UUID verification", + ); + } + } + + if let Some(template) = extra.template() { + if !template.is_valid() { + error_report.functional_validation( + &format!("Document Template {template:?} is invalid"), + "During Document Template UUID verification", + ); + } + } + + if let Some(reply) = extra.reply() { + if !reply.is_valid() { + error_report.functional_validation( + &format!("Document Reply {reply:?} is invalid"), + "During Document Reply UUID verification", + ); + } + } + + if let Some(brand_id) = extra.brand_id() { + if !brand_id.is_valid() { + error_report.functional_validation( + &format!("Document Brand ID {brand_id:?} is invalid"), + "During Document Brand ID UUID verification", + ); + } + } + + if let Some(campaign_id) = extra.campaign_id() { + if !campaign_id.is_valid() { + error_report.functional_validation( + &format!("Document Campaign ID {campaign_id:?} is invalid"), + "During Document Campaign ID UUID verification", + ); + } + } + + if let Some(election_id) = extra.election_id() { + if !election_id.is_valid() { + error_report.functional_validation( + &format!("Document Election ID {election_id:?} is invalid"), + "During Document Election ID UUID verification", + ); + } + } + + if let Some(category_id) = extra.category_id() { + if !category_id.is_valid() { + error_report.functional_validation( + &format!("Document Category ID {category_id:?} is invalid"), + "During Document Category ID UUID verification", + ); + } + } + match self.as_cose_sign() { Ok(cose_sign) => { let signatures = self.signatures().cose_signatures(); diff --git a/rust/signed_doc/src/metadata/document_ref.rs b/rust/signed_doc/src/metadata/document_ref.rs index 367e66a3c6..eaffaa58b2 100644 --- a/rust/signed_doc/src/metadata/document_ref.rs +++ b/rust/signed_doc/src/metadata/document_ref.rs @@ -13,6 +13,17 @@ pub struct DocumentRef { pub ver: Option, } +impl DocumentRef { + /// Determine if internal `UUID`s are valid. + #[must_use] + pub fn is_valid(&self) -> bool { + match self.ver { + Some(ver) => self.id.is_valid() && ver.is_valid() && ver >= self.id, + None => self.id.is_valid(), + } + } +} + impl TryFrom for Value { type Error = anyhow::Error; From 22c46f0215c8d24cf1484e135c709b2be095926c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Rosales?= Date: Mon, 27 Jan 2025 22:12:04 -0600 Subject: [PATCH 08/17] feat(rust/signed-doc): add signatures to Catalyst Signed Document --- rust/signed_doc/src/lib.rs | 40 +++++++++++++++++++++++++++- rust/signed_doc/src/metadata/mod.rs | 6 +++++ rust/signed_doc/src/signature/mod.rs | 5 ++++ 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/rust/signed_doc/src/lib.rs b/rust/signed_doc/src/lib.rs index f91c65cdac..9910232576 100644 --- a/rust/signed_doc/src/lib.rs +++ b/rust/signed_doc/src/lib.rs @@ -17,7 +17,7 @@ pub use builder::Builder; use catalyst_types::problem_report::ProblemReport; pub use content::Content; use coset::{CborSerializable, Header}; -use ed25519_dalek::VerifyingKey; +use ed25519_dalek::{ed25519::signature::Signer, SecretKey, VerifyingKey}; use error::CatalystSignedDocError; pub use metadata::{DocumentRef, ExtraFields, Metadata, UuidV4, UuidV7}; pub use minicbor::{decode, encode, Decode, Decoder, Encode}; @@ -259,6 +259,44 @@ impl CatalystSignedDocument { Ok(()) } + /// Add a signature to the Catalyst Signed Document. + /// + /// # Returns + /// + /// A new Catalyst Signed Document with the added signature. + /// + /// # Errors + /// + /// Fails if the current signed document cannot be encoded as COSE SIGN, + /// or if the Arc inner value cannot be obtained. + pub fn sign(self, sk: SecretKey, kid: KidUri) -> anyhow::Result { + let cose_sign = self.as_cose_sign()?; + let Some(InnerCatalystSignedDocument { + metadata, + content, + mut signatures, + }) = Arc::into_inner(self.inner) + else { + anyhow::bail!("Failed to extract inner signed document"); + }; + let sk = ed25519_dalek::SigningKey::from_bytes(&sk); + let protected_header = coset::HeaderBuilder::new() + .key_id(kid.to_string().into_bytes()) + .algorithm(metadata.algorithm().into()); + let mut signature = coset::CoseSignatureBuilder::new() + .protected(protected_header.build()) + .build(); + let data_to_sign = cose_sign.tbs_data(&[], &signature); + signature.signature = sk.sign(&data_to_sign).to_vec(); + signatures.push(kid, signature); + Ok(InnerCatalystSignedDocument { + metadata, + content, + signatures, + } + .into()) + } + /// Convert Catalyst Signed Document into `coset::CoseSign` fn as_cose_sign(&self) -> anyhow::Result { let mut cose_bytes: Vec = Vec::new(); diff --git a/rust/signed_doc/src/metadata/mod.rs b/rust/signed_doc/src/metadata/mod.rs index 6119368113..a770470cec 100644 --- a/rust/signed_doc/src/metadata/mod.rs +++ b/rust/signed_doc/src/metadata/mod.rs @@ -59,6 +59,12 @@ pub struct Metadata { } impl Metadata { + /// Return Document Cryptographic Algorithm + #[must_use] + pub fn algorithm(&self) -> Algorithm { + self.alg + } + /// Return Document Type `UUIDv4`. #[must_use] pub fn doc_type(&self) -> UuidV4 { diff --git a/rust/signed_doc/src/signature/mod.rs b/rust/signed_doc/src/signature/mod.rs index 3c1ff99c7d..6645ca3d23 100644 --- a/rust/signed_doc/src/signature/mod.rs +++ b/rust/signed_doc/src/signature/mod.rs @@ -37,6 +37,11 @@ impl Signatures { self.0.iter().map(|sig| sig.signature.clone()).collect() } + /// Add a new signature + pub fn push(&mut self, kid: KidUri, signature: CoseSignature) { + self.0.push(Signature { kid, signature }); + } + /// Number of signatures. #[must_use] pub fn len(&self) -> usize { From 0f850575bda1fdccc805cfa0725bb948580090dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Rosales?= Date: Wed, 29 Jan 2025 12:44:06 -0600 Subject: [PATCH 09/17] fix(rust/signed-doc): refactor cli tool to sign --- rust/signed_doc/examples/mk_signed_doc.rs | 89 ++++++++++------------- 1 file changed, 37 insertions(+), 52 deletions(-) diff --git a/rust/signed_doc/examples/mk_signed_doc.rs b/rust/signed_doc/examples/mk_signed_doc.rs index 4dfc0af6e0..e9a51cfb40 100644 --- a/rust/signed_doc/examples/mk_signed_doc.rs +++ b/rust/signed_doc/examples/mk_signed_doc.rs @@ -71,29 +71,23 @@ impl Cli { .with_content(payload) .with_metadata(metadata) .build()?; - let mut bytes: Vec = Vec::new(); - minicbor::encode(signed_doc, &mut bytes) - .map_err(|e| anyhow::anyhow!("Failed to encode document: {e}"))?; - - write_bytes_to_file(&bytes, &output)?; + save_signed_doc(signed_doc, &output)?; }, Self::Sign { sk, doc, kid } => { let sk = load_secret_key_from_file(&sk) .map_err(|e| anyhow::anyhow!("Failed to load SK FILE: {e}"))?; - let mut cose = load_cose_from_file(&doc) - .map_err(|e| anyhow::anyhow!("Failed to load COSE FROM FILE: {e}"))?; - add_signature_to_cose(&mut cose, &sk, kid.to_string()); - store_cose_file(cose, &doc)?; + let cose_bytes = read_bytes_from_file(&doc)?; + let signed_doc = signed_doc_from_bytes(cose_bytes.as_slice())?; + let new_signed_doc = signed_doc.sign(sk.to_bytes(), kid)?; + save_signed_doc(new_signed_doc, &doc)?; }, Self::Inspect { path } => { - let mut cose_file = File::open(path)?; - let mut cose_bytes = Vec::new(); - cose_file.read_to_end(&mut cose_bytes)?; - decode_signed_doc(&cose_bytes); + let cose_bytes = read_bytes_from_file(&path)?; + inspect_signed_doc(&cose_bytes)?; }, Self::InspectBytes { cose_sign_hex } => { let cose_bytes = hex::decode(&cose_sign_hex)?; - decode_signed_doc(&cose_bytes); + inspect_signed_doc(&cose_bytes)?; }, } println!("Done"); @@ -101,20 +95,36 @@ impl Cli { } } -fn decode_signed_doc(cose_bytes: &[u8]) { +fn read_bytes_from_file(path: &PathBuf) -> anyhow::Result> { + let mut cose_file = File::open(path)?; + let mut cose_bytes = Vec::new(); + cose_file.read_to_end(&mut cose_bytes)?; + Ok(cose_bytes) +} + +fn inspect_signed_doc(cose_bytes: &[u8]) -> anyhow::Result<()> { println!( - "Decoding {} bytes: {}", + "Decoding {} bytes:\n{}", cose_bytes.len(), hex::encode(cose_bytes) ); + let cat_signed_doc = signed_doc_from_bytes(cose_bytes)?; + println!("This is a valid Catalyst Document."); + println!("{cat_signed_doc}"); + Ok(()) +} - match CatalystSignedDocument::try_from(cose_bytes) { - Ok(cat_signed_doc) => { - println!("This is a valid Catalyst Document."); - println!("{cat_signed_doc}"); - }, - Err(e) => eprintln!("Invalid Catalyst Document, err: {e}"), - } +fn save_signed_doc(signed_doc: CatalystSignedDocument, path: &PathBuf) -> anyhow::Result<()> { + let mut bytes: Vec = Vec::new(); + minicbor::encode(signed_doc, &mut bytes) + .map_err(|e| anyhow::anyhow!("Failed to encode document: {e}"))?; + + write_bytes_to_file(&bytes, path) +} + +fn signed_doc_from_bytes(cose_bytes: &[u8]) -> anyhow::Result { + CatalystSignedDocument::try_from(cose_bytes) + .map_err(|e| anyhow::anyhow!("Invalid Catalyst Document: {e}")) } fn load_json_from_file(path: &PathBuf) -> anyhow::Result @@ -124,45 +134,20 @@ where T: for<'de> serde::Deserialize<'de> { Ok(json) } -fn load_cose_from_file(cose_path: &PathBuf) -> anyhow::Result { - let cose_file_bytes = read_bytes_from_file(cose_path)?; - let cose = coset::CoseSign::from_slice(&cose_file_bytes).map_err(|e| anyhow::anyhow!("{e}"))?; - Ok(cose) -} - -fn read_bytes_from_file(path: &PathBuf) -> anyhow::Result> { - let mut file_bytes = Vec::new(); - File::open(path)?.read_to_end(&mut file_bytes)?; - Ok(file_bytes) -} - fn write_bytes_to_file(bytes: &[u8], output: &PathBuf) -> anyhow::Result<()> { File::create(output)? .write_all(bytes) .map_err(|e| anyhow::anyhow!("Failed to write to file {output:?}: {e}")) } -fn store_cose_file(cose: coset::CoseSign, output: &PathBuf) -> anyhow::Result<()> { - let cose_bytes = cose - .to_vec() - .map_err(|e| anyhow::anyhow!("Failed to Store COSE SIGN: {e}"))?; - write_bytes_to_file(&cose_bytes, output) -} - fn load_secret_key_from_file(sk_path: &PathBuf) -> anyhow::Result { let sk_str = read_to_string(sk_path)?; let sk = ed25519_dalek::SigningKey::from_pkcs8_pem(&sk_str)?; Ok(sk) } -fn add_signature_to_cose(cose: &mut coset::CoseSign, sk: &ed25519_dalek::SigningKey, kid: String) { - let protected_header = coset::HeaderBuilder::new() - .key_id(kid.into_bytes()) - .algorithm(coset::iana::Algorithm::EdDSA); - let mut signature = coset::CoseSignatureBuilder::new() - .protected(protected_header.build()) - .build(); - let data_to_sign = cose.tbs_data(&[], &signature); - signature.signature = sk.sign(&data_to_sign).to_vec(); - cose.signatures.push(signature); +fn load_public_key_from_file(pk_path: &PathBuf) -> anyhow::Result { + let pk_str = read_to_string(pk_path)?; + let pk = ed25519_dalek::VerifyingKey::from_public_key_pem(&pk_str)?; + Ok(pk) } From ee0657cadf9138c8f2b9150046893db2eb2f956d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Rosales?= Date: Wed, 29 Jan 2025 12:48:58 -0600 Subject: [PATCH 10/17] fix(rust/signed-doc): add verify command to cli tool --- rust/signed_doc/examples/mk_signed_doc.rs | 29 +++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/rust/signed_doc/examples/mk_signed_doc.rs b/rust/signed_doc/examples/mk_signed_doc.rs index e9a51cfb40..55153c15c5 100644 --- a/rust/signed_doc/examples/mk_signed_doc.rs +++ b/rust/signed_doc/examples/mk_signed_doc.rs @@ -10,8 +10,7 @@ use std::{ use catalyst_signed_doc::{Builder, CatalystSignedDocument, KidUri, Metadata}; use clap::Parser; -use coset::CborSerializable; -use ed25519_dalek::{ed25519::signature::Signer, pkcs8::DecodePrivateKey}; +use ed25519_dalek::pkcs8::{DecodePrivateKey, DecodePublicKey}; fn main() { if let Err(err) = Cli::parse().exec() { @@ -52,6 +51,16 @@ enum Cli { /// Hex-formatted COSE SIGN Bytes cose_sign_hex: String, }, + /// Validates a signature by Key ID and verifiying key + Verify { + /// Path to the formed (could be empty, without any signatures) COSE document + /// This exact file would be modified and new signature would be added + path: PathBuf, + /// Path to the verifying key in PEM format + pk: PathBuf, + /// Signer kid + kid: KidUri, + }, } impl Cli { @@ -89,6 +98,22 @@ impl Cli { let cose_bytes = hex::decode(&cose_sign_hex)?; inspect_signed_doc(&cose_bytes)?; }, + Self::Verify { path, pk, kid } => { + let pk = load_public_key_from_file(&pk) + .map_err(|e| anyhow::anyhow!("Failed to load PK FILE {pk:?}: {e}"))?; + let cose_bytes = read_bytes_from_file(&path)?; + let signed_doc = signed_doc_from_bytes(cose_bytes.as_slice())?; + signed_doc + .verify(|k| { + if k.to_string() == kid.to_string() { + pk + } else { + k.role0_pk() + } + }) + .map_err(|e| anyhow::anyhow!("Catalyst Document Verification failed: {e}"))?; + println!("Catalyst Signed Document is Verified."); + }, } println!("Done"); Ok(()) From 262b1ad3a890cbb701f832c0a6ee91a35c6b70f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Rosales?= Date: Wed, 29 Jan 2025 14:28:40 -0600 Subject: [PATCH 11/17] fix(rust/catalyst-types): refactor sigining logic into builder --- rust/signed_doc/examples/mk_signed_doc.rs | 5 +- rust/signed_doc/src/builder.rs | 60 ++++++++++++++++++++--- rust/signed_doc/src/lib.rs | 49 +++++------------- 3 files changed, 68 insertions(+), 46 deletions(-) diff --git a/rust/signed_doc/examples/mk_signed_doc.rs b/rust/signed_doc/examples/mk_signed_doc.rs index 55153c15c5..7b1e3cf960 100644 --- a/rust/signed_doc/examples/mk_signed_doc.rs +++ b/rust/signed_doc/examples/mk_signed_doc.rs @@ -77,7 +77,7 @@ impl Cli { let payload = serde_json::to_vec(&json_doc)?; // Start with no signatures. let signed_doc = Builder::new() - .with_content(payload) + .with_decoded_content(payload) .with_metadata(metadata) .build()?; save_signed_doc(signed_doc, &output)?; @@ -87,7 +87,8 @@ impl Cli { .map_err(|e| anyhow::anyhow!("Failed to load SK FILE: {e}"))?; let cose_bytes = read_bytes_from_file(&doc)?; let signed_doc = signed_doc_from_bytes(cose_bytes.as_slice())?; - let new_signed_doc = signed_doc.sign(sk.to_bytes(), kid)?; + let builder = signed_doc.as_signed_doc_builder(); + let new_signed_doc = builder.add_signature(sk.to_bytes(), kid)?.build()?; save_signed_doc(new_signed_doc, &doc)?; }, Self::Inspect { path } => { diff --git a/rust/signed_doc/src/builder.rs b/rust/signed_doc/src/builder.rs index 34ddcc1678..50e72ba9bc 100644 --- a/rust/signed_doc/src/builder.rs +++ b/rust/signed_doc/src/builder.rs @@ -1,15 +1,18 @@ //! Catalyst Signed Document Builder. +use catalyst_types::kid_uri::KidUri; +use ed25519_dalek::{ed25519::signature::Signer, SecretKey}; + use crate::{CatalystSignedDocument, Content, InnerCatalystSignedDocument, Metadata, Signatures}; /// Catalyst Signed Document Builder. -#[derive(Debug, Default)] +#[derive(Debug, Default, Clone)] pub struct Builder { /// Document Metadata - metadata: Option, + pub(crate) metadata: Option, /// Document Content - content: Option>, + pub(crate) content: Option>, /// Signatures - signatures: Signatures, + pub(crate) signatures: Signatures, } impl Builder { @@ -26,13 +29,58 @@ impl Builder { self } - /// Set document content + /// Set decoded (original) document content bytes #[must_use] - pub fn with_content(mut self, content: Vec) -> Self { + pub fn with_decoded_content(mut self, content: Vec) -> Self { self.content = Some(content); self } + /// Set document signatures + #[must_use] + pub fn with_signatures(mut self, signatures: Signatures) -> Self { + self.signatures = signatures; + self + } + + /// Add a signature to the document + /// + /// # Errors + /// + /// Fails if a `CatalystSignedDocument` cannot be created due to missing metadata or + /// content, due to malformed data, or when the signed document cannot be + /// converted into `coset::CoseSign`. + pub fn add_signature(self, sk: SecretKey, kid: KidUri) -> anyhow::Result { + let cose_sign = self + .clone() + .build() + .map_err(|e| anyhow::anyhow!("Failed to sign: {e}"))? + .as_cose_sign() + .map_err(|e| anyhow::anyhow!("Failed to sign: {e}"))?; + let Self { + metadata: Some(metadata), + content: Some(content), + mut signatures, + } = self + else { + anyhow::bail!("Metadata and Content are needed for signing"); + }; + let sk = ed25519_dalek::SigningKey::from_bytes(&sk); + let protected_header = coset::HeaderBuilder::new() + .key_id(kid.to_string().into_bytes()) + .algorithm(metadata.algorithm().into()); + let mut signature = coset::CoseSignatureBuilder::new() + .protected(protected_header.build()) + .build(); + let data_to_sign = cose_sign.tbs_data(&[], &signature); + signature.signature = sk.sign(&data_to_sign).to_vec(); + signatures.push(kid, signature); + Ok(Self::new() + .with_decoded_content(content) + .with_metadata(metadata) + .with_signatures(signatures)) + } + /// Build a signed document /// /// ## Errors diff --git a/rust/signed_doc/src/lib.rs b/rust/signed_doc/src/lib.rs index 9910232576..64af3b93ae 100644 --- a/rust/signed_doc/src/lib.rs +++ b/rust/signed_doc/src/lib.rs @@ -17,7 +17,7 @@ pub use builder::Builder; use catalyst_types::problem_report::ProblemReport; pub use content::Content; use coset::{CborSerializable, Header}; -use ed25519_dalek::{ed25519::signature::Signer, SecretKey, VerifyingKey}; +use ed25519_dalek::VerifyingKey; use error::CatalystSignedDocError; pub use metadata::{DocumentRef, ExtraFields, Metadata, UuidV4, UuidV7}; pub use minicbor::{decode, encode, Decode, Decoder, Encode}; @@ -259,42 +259,15 @@ impl CatalystSignedDocument { Ok(()) } - /// Add a signature to the Catalyst Signed Document. - /// - /// # Returns - /// - /// A new Catalyst Signed Document with the added signature. - /// - /// # Errors - /// - /// Fails if the current signed document cannot be encoded as COSE SIGN, - /// or if the Arc inner value cannot be obtained. - pub fn sign(self, sk: SecretKey, kid: KidUri) -> anyhow::Result { - let cose_sign = self.as_cose_sign()?; - let Some(InnerCatalystSignedDocument { - metadata, - content, - mut signatures, - }) = Arc::into_inner(self.inner) - else { - anyhow::bail!("Failed to extract inner signed document"); - }; - let sk = ed25519_dalek::SigningKey::from_bytes(&sk); - let protected_header = coset::HeaderBuilder::new() - .key_id(kid.to_string().into_bytes()) - .algorithm(metadata.algorithm().into()); - let mut signature = coset::CoseSignatureBuilder::new() - .protected(protected_header.build()) - .build(); - let data_to_sign = cose_sign.tbs_data(&[], &signature); - signature.signature = sk.sign(&data_to_sign).to_vec(); - signatures.push(kid, signature); - Ok(InnerCatalystSignedDocument { - metadata, - content, - signatures, + /// Returns a signed document `Builder` pre-loaded with the current signed document's + /// data. + #[must_use] + pub fn as_signed_doc_builder(&self) -> Builder { + Builder { + metadata: Some(self.inner.metadata.clone()), + content: Some(self.inner.content.decoded_bytes().to_vec()), + signatures: self.inner.signatures.clone(), } - .into()) } /// Convert Catalyst Signed Document into `coset::CoseSign` @@ -302,7 +275,7 @@ impl CatalystSignedDocument { let mut cose_bytes: Vec = Vec::new(); minicbor::encode(self, &mut cose_bytes)?; coset::CoseSign::from_slice(&cose_bytes) - .map_err(|e| anyhow::anyhow!("encoding COSE SIGN failed: {e}")) + .map_err(|e| anyhow::anyhow!("encoding as COSE SIGN failed: {e}")) } } @@ -477,7 +450,7 @@ mod tests { let doc = Builder::new() .with_metadata(metadata.clone()) - .with_content(content.clone()) + .with_decoded_content(content.clone()) .build() .unwrap(); From 56b0509852ceae3d0da3dc4e15968f309b1f5dfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Rosales?= Date: Wed, 29 Jan 2025 14:49:44 -0600 Subject: [PATCH 12/17] chore(docs): fix spelling --- rust/signed_doc/examples/mk_signed_doc.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/signed_doc/examples/mk_signed_doc.rs b/rust/signed_doc/examples/mk_signed_doc.rs index 7b1e3cf960..6bc5a952bd 100644 --- a/rust/signed_doc/examples/mk_signed_doc.rs +++ b/rust/signed_doc/examples/mk_signed_doc.rs @@ -51,7 +51,7 @@ enum Cli { /// Hex-formatted COSE SIGN Bytes cose_sign_hex: String, }, - /// Validates a signature by Key ID and verifiying key + /// Validates a signature by Key ID and verifying key Verify { /// Path to the formed (could be empty, without any signatures) COSE document /// This exact file would be modified and new signature would be added From 70cae9f48a24bbe15f139a94ef8627af59f1dad7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Rosales?= Date: Thu, 30 Jan 2025 21:21:00 -0600 Subject: [PATCH 13/17] fix(rust/signed-doc): cleanup --- rust/signed_doc/examples/mk_signed_doc.rs | 2 +- rust/signed_doc/src/builder.rs | 6 +- rust/signed_doc/src/lib.rs | 107 ++-------------------- 3 files changed, 10 insertions(+), 105 deletions(-) diff --git a/rust/signed_doc/examples/mk_signed_doc.rs b/rust/signed_doc/examples/mk_signed_doc.rs index 6bc5a952bd..25215c2a70 100644 --- a/rust/signed_doc/examples/mk_signed_doc.rs +++ b/rust/signed_doc/examples/mk_signed_doc.rs @@ -87,7 +87,7 @@ impl Cli { .map_err(|e| anyhow::anyhow!("Failed to load SK FILE: {e}"))?; let cose_bytes = read_bytes_from_file(&doc)?; let signed_doc = signed_doc_from_bytes(cose_bytes.as_slice())?; - let builder = signed_doc.as_signed_doc_builder(); + let builder = signed_doc.into_builder(); let new_signed_doc = builder.add_signature(sk.to_bytes(), kid)?.build()?; save_signed_doc(new_signed_doc, &doc)?; }, diff --git a/rust/signed_doc/src/builder.rs b/rust/signed_doc/src/builder.rs index 50e72ba9bc..e16076d98c 100644 --- a/rust/signed_doc/src/builder.rs +++ b/rust/signed_doc/src/builder.rs @@ -8,11 +8,11 @@ use crate::{CatalystSignedDocument, Content, InnerCatalystSignedDocument, Metada #[derive(Debug, Default, Clone)] pub struct Builder { /// Document Metadata - pub(crate) metadata: Option, + metadata: Option, /// Document Content - pub(crate) content: Option>, + content: Option>, /// Signatures - pub(crate) signatures: Signatures, + signatures: Signatures, } impl Builder { diff --git a/rust/signed_doc/src/lib.rs b/rust/signed_doc/src/lib.rs index 64af3b93ae..e59307e8d3 100644 --- a/rust/signed_doc/src/lib.rs +++ b/rust/signed_doc/src/lib.rs @@ -107,7 +107,7 @@ impl CatalystSignedDocument { &self.inner.signatures } - /// Verify document signatures and `UUID`s. + /// Verify document signatures. /// /// # Errors /// @@ -117,100 +117,6 @@ impl CatalystSignedDocument { where P: Fn(&KidUri) -> VerifyingKey { let error_report = ProblemReport::new("Catalyst Signed Document Verification"); - if !self.doc_type().is_valid() { - error_report.functional_validation( - &format!("{} is invalid UUIDv4", self.doc_type()), - "During Document Type UUID verification", - ); - } - - let doc_id = self.doc_id(); - if !doc_id.is_valid() { - error_report.functional_validation( - &format!("{doc_id} is invalid UUIDv7"), - "During Document ID UUID verification", - ); - } - - let doc_ver = self.doc_ver(); - if !doc_ver.is_valid() { - error_report.functional_validation( - &format!("{doc_ver} is invalid UUIDv7"), - "During Document Version UUID verification", - ); - } - - if doc_id.is_valid() && doc_ver.is_valid() && doc_ver < doc_id { - error_report.functional_validation( - &format!("Document Version {doc_ver} is smaller than Document Id {doc_id}"), - "During Document Version UUID verification", - ); - } - - let extra = self.doc_meta(); - if let Some(doc_ref) = extra.doc_ref() { - if !doc_ref.is_valid() { - error_report.functional_validation( - &format!("Document Reference {doc_ref:?} is invalid"), - "During Document Reference UUID verification", - ); - } - } - - if let Some(template) = extra.template() { - if !template.is_valid() { - error_report.functional_validation( - &format!("Document Template {template:?} is invalid"), - "During Document Template UUID verification", - ); - } - } - - if let Some(reply) = extra.reply() { - if !reply.is_valid() { - error_report.functional_validation( - &format!("Document Reply {reply:?} is invalid"), - "During Document Reply UUID verification", - ); - } - } - - if let Some(brand_id) = extra.brand_id() { - if !brand_id.is_valid() { - error_report.functional_validation( - &format!("Document Brand ID {brand_id:?} is invalid"), - "During Document Brand ID UUID verification", - ); - } - } - - if let Some(campaign_id) = extra.campaign_id() { - if !campaign_id.is_valid() { - error_report.functional_validation( - &format!("Document Campaign ID {campaign_id:?} is invalid"), - "During Document Campaign ID UUID verification", - ); - } - } - - if let Some(election_id) = extra.election_id() { - if !election_id.is_valid() { - error_report.functional_validation( - &format!("Document Election ID {election_id:?} is invalid"), - "During Document Election ID UUID verification", - ); - } - } - - if let Some(category_id) = extra.category_id() { - if !category_id.is_valid() { - error_report.functional_validation( - &format!("Document Category ID {category_id:?} is invalid"), - "During Document Category ID UUID verification", - ); - } - } - match self.as_cose_sign() { Ok(cose_sign) => { let signatures = self.signatures().cose_signatures(); @@ -262,12 +168,11 @@ impl CatalystSignedDocument { /// Returns a signed document `Builder` pre-loaded with the current signed document's /// data. #[must_use] - pub fn as_signed_doc_builder(&self) -> Builder { - Builder { - metadata: Some(self.inner.metadata.clone()), - content: Some(self.inner.content.decoded_bytes().to_vec()), - signatures: self.inner.signatures.clone(), - } + pub fn into_builder(self) -> Builder { + Builder::new() + .with_metadata(self.inner.metadata.clone()) + .with_decoded_content(self.inner.content.decoded_bytes().to_vec()) + .with_signatures(self.inner.signatures.clone()) } /// Convert Catalyst Signed Document into `coset::CoseSign` From 0329541a4fefc1581929176a10ad13a9255f4f2c Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Sun, 2 Feb 2025 15:39:37 +0200 Subject: [PATCH 14/17] fix content validation, add unit test --- rust/signed_doc/src/content.rs | 22 ++---------- rust/signed_doc/src/lib.rs | 2 +- rust/signed_doc/src/metadata/content_type.rs | 35 ++++++++++++++++++++ 3 files changed, 39 insertions(+), 20 deletions(-) diff --git a/rust/signed_doc/src/content.rs b/rust/signed_doc/src/content.rs index 2bfb4a9781..92c644e158 100644 --- a/rust/signed_doc/src/content.rs +++ b/rust/signed_doc/src/content.rs @@ -28,6 +28,7 @@ impl Content { .decode(&data) .map_err(|e| anyhow::anyhow!("Failed to decode {encoding} content: {e}"))?; } + content_type.validate(&data)?; Ok(Self { data, @@ -44,18 +45,7 @@ impl Content { pub(crate) fn from_decoded( data: Vec, content_type: ContentType, content_encoding: Option, ) -> anyhow::Result { - match content_type { - ContentType::Json => { - if let Err(e) = serde_json::to_value(&data) { - anyhow::bail!("Invalid {content_type} content: {e}") - } - }, - ContentType::Cbor => { - if let Err(e) = minicbor::decode::(&data) { - anyhow::bail!("Invalid {content_type} content: {e}") - } - }, - } + content_type.validate(&data)?; Ok(Self { data, content_type, @@ -97,13 +87,7 @@ impl Content { /// Return content byte size #[must_use] - pub fn len(&self) -> usize { + pub fn size(&self) -> usize { self.data.len() } - - /// Return `true` if content is empty - #[must_use] - pub fn is_empty(&self) -> bool { - self.data.is_empty() - } } diff --git a/rust/signed_doc/src/lib.rs b/rust/signed_doc/src/lib.rs index e59307e8d3..80937dc81c 100644 --- a/rust/signed_doc/src/lib.rs +++ b/rust/signed_doc/src/lib.rs @@ -47,7 +47,7 @@ pub struct CatalystSignedDocument { impl Display for CatalystSignedDocument { fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { writeln!(f, "{}", self.inner.metadata)?; - writeln!(f, "Payload Size: {} bytes", self.inner.content.len())?; + writeln!(f, "Payload Size: {} bytes", self.inner.content.size())?; writeln!(f, "Signature Information")?; if self.inner.signatures.is_empty() { writeln!(f, " This document is unsigned.")?; diff --git a/rust/signed_doc/src/metadata/content_type.rs b/rust/signed_doc/src/metadata/content_type.rs index 40239692ce..10123ba17f 100644 --- a/rust/signed_doc/src/metadata/content_type.rs +++ b/rust/signed_doc/src/metadata/content_type.rs @@ -18,6 +18,25 @@ pub enum ContentType { Json, } +impl ContentType { + /// Validates the provided `content` bytes to be a defined `ContentType`. + pub fn validate(self, content: &[u8]) -> anyhow::Result<()> { + match self { + Self::Json => { + if let Err(e) = serde_json::from_slice::(content) { + anyhow::bail!("Invalid {self} content: {e}") + } + }, + Self::Cbor => { + if let Err(e) = minicbor::decode::(content) { + anyhow::bail!("Invalid {self} content: {e}") + } + }, + } + Ok(()) + } +} + impl Display for ContentType { fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { match self { @@ -78,3 +97,19 @@ impl TryFrom<&coset::ContentType> for ContentType { Ok(content_type) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn content_type_validate_test() { + let json_bytes = serde_json::to_vec(&serde_json::Value::Null).unwrap(); + assert!(ContentType::Json.validate(&json_bytes).is_ok()); + assert!(ContentType::Cbor.validate(&json_bytes).is_err()); + + let cbor_bytes = minicbor::to_vec(minicbor::data::Token::Null).unwrap(); + assert!(ContentType::Json.validate(&cbor_bytes).is_err()); + assert!(ContentType::Cbor.validate(&cbor_bytes).is_ok()); + } +} From 3d1b04454e5be88876e148544c3c61933822ea15 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Sun, 2 Feb 2025 15:46:29 +0200 Subject: [PATCH 15/17] remove clippy --- rust/signed_doc/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/signed_doc/src/lib.rs b/rust/signed_doc/src/lib.rs index 80937dc81c..e203e0856c 100644 --- a/rust/signed_doc/src/lib.rs +++ b/rust/signed_doc/src/lib.rs @@ -112,7 +112,7 @@ impl CatalystSignedDocument { /// # Errors /// /// Returns a report of verification failures and the source error. - #[allow(clippy::indexing_slicing, clippy::too_many_lines)] + #[allow(clippy::indexing_slicing)] pub fn verify

(&self, pk_getter: P) -> Result<(), CatalystSignedDocError> where P: Fn(&KidUri) -> VerifyingKey { let error_report = ProblemReport::new("Catalyst Signed Document Verification"); From 5682dd5cf9588e92992c11ea3c9f6dc58ad45da5 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Sun, 2 Feb 2025 16:16:47 +0200 Subject: [PATCH 16/17] fix --- rust/signed_doc/src/lib.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/rust/signed_doc/src/lib.rs b/rust/signed_doc/src/lib.rs index e203e0856c..5c47c821c9 100644 --- a/rust/signed_doc/src/lib.rs +++ b/rust/signed_doc/src/lib.rs @@ -351,7 +351,7 @@ mod tests { "category_id": {"id": uuid_v7.to_string()}, })) .unwrap(); - let content = vec![1, 2, 4, 5, 6, 7, 8, 9]; + let content = serde_json::to_vec(&serde_json::Value::Null).unwrap(); let doc = Builder::new() .with_metadata(metadata.clone()) @@ -359,9 +359,7 @@ mod tests { .build() .unwrap(); - let mut bytes = Vec::new(); - minicbor::encode_with(doc, &mut bytes, &mut ()).unwrap(); - + let bytes = minicbor::to_vec(doc).unwrap(); let decoded: CatalystSignedDocument = bytes.as_slice().try_into().unwrap(); assert_eq!(decoded.doc_type(), uuid_v4); From a2af6f16ea73d6cc6fcfbe94ee0fae3a549b2661 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Rosales?= Date: Sun, 2 Feb 2025 17:39:40 -0600 Subject: [PATCH 17/17] feat(rust/signed-doc): test verify signatures --- rust/signed_doc/Cargo.toml | 1 + rust/signed_doc/src/lib.rs | 48 ++++++++++++++++++-- rust/signed_doc/src/metadata/document_ref.rs | 11 ----- 3 files changed, 46 insertions(+), 14 deletions(-) diff --git a/rust/signed_doc/Cargo.toml b/rust/signed_doc/Cargo.toml index f44c177934..8bbfb181e7 100644 --- a/rust/signed_doc/Cargo.toml +++ b/rust/signed_doc/Cargo.toml @@ -26,6 +26,7 @@ clap = { version = "4.5.23", features = ["derive", "env"] } [dev-dependencies] +base64-url = "3.0.0" rand = "0.8.5" diff --git a/rust/signed_doc/src/lib.rs b/rust/signed_doc/src/lib.rs index 5c47c821c9..ff2bf290f0 100644 --- a/rust/signed_doc/src/lib.rs +++ b/rust/signed_doc/src/lib.rs @@ -321,12 +321,15 @@ impl Encode<()> for CatalystSignedDocument { #[cfg(test)] mod tests { + use std::str::FromStr; + + use ed25519_dalek::SigningKey; use metadata::{ContentEncoding, ContentType}; + use rand::rngs::OsRng; use super::*; - #[test] - fn catalyst_signed_doc_cbor_roundtrip_test() { + fn test_metadata() -> anyhow::Result<(UuidV7, UuidV4, Metadata)> { let uuid_v7 = UuidV7::new(); let uuid_v4 = UuidV4::new(); let section = "some section".to_string(); @@ -350,7 +353,13 @@ mod tests { "brand_id": {"id": uuid_v7.to_string()}, "category_id": {"id": uuid_v7.to_string()}, })) - .unwrap(); + .map_err(|_| anyhow::anyhow!("Invalid example metadata. This should not happen."))?; + Ok((uuid_v7, uuid_v4, metadata)) + } + + #[test] + fn catalyst_signed_doc_cbor_roundtrip_test() { + let (uuid_v7, uuid_v4, metadata) = test_metadata().unwrap(); let content = serde_json::to_vec(&serde_json::Value::Null).unwrap(); let doc = Builder::new() @@ -368,4 +377,37 @@ mod tests { assert_eq!(decoded.doc_content().decoded_bytes(), &content); assert_eq!(decoded.doc_meta(), metadata.extra()); } + + #[test] + fn signature_verification_test() { + let mut csprng = OsRng; + let sk: SigningKey = SigningKey::generate(&mut csprng); + let content = serde_json::to_vec(&serde_json::Value::Null).unwrap(); + let pk = sk.verifying_key(); + + let kid_str = format!( + "kid.catalyst-rbac://cardano/{}/0/0", + base64_url::encode(pk.as_bytes()) + ); + + let kid = KidUri::from_str(&kid_str).unwrap(); + let (_, _, metadata) = test_metadata().unwrap(); + let signed_doc = Builder::new() + .with_decoded_content(content) + .with_metadata(metadata) + .add_signature(sk.to_bytes(), kid.clone()) + .unwrap() + .build() + .unwrap(); + + assert!(signed_doc + .verify(|k| { + if k.to_string() == kid.to_string() { + pk + } else { + k.role0_pk() + } + }) + .is_ok()); + } } diff --git a/rust/signed_doc/src/metadata/document_ref.rs b/rust/signed_doc/src/metadata/document_ref.rs index eaffaa58b2..367e66a3c6 100644 --- a/rust/signed_doc/src/metadata/document_ref.rs +++ b/rust/signed_doc/src/metadata/document_ref.rs @@ -13,17 +13,6 @@ pub struct DocumentRef { pub ver: Option, } -impl DocumentRef { - /// Determine if internal `UUID`s are valid. - #[must_use] - pub fn is_valid(&self) -> bool { - match self.ver { - Some(ver) => self.id.is_valid() && ver.is_valid() && ver >= self.id, - None => self.id.is_valid(), - } - } -} - impl TryFrom for Value { type Error = anyhow::Error;