Skip to content

Commit de7680f

Browse files
authored
feat(rust/signed-doc): Added unit tests for Catalyst Signed Docs validation rules (#219)
* Squashed commit of the following: commit f2273d3 Author: Mr-Leshiy <[email protected]> Date: Thu Feb 20 10:35:14 2025 +0200 fix clippy lints commit 5b42d74 Author: Mr-Leshiy <[email protected]> Date: Thu Feb 20 10:07:12 2025 +0200 fix spelling commit 8121f33 Merge: b7bbe15 f76ccf3 Author: Alex Pozhylenkov <[email protected]> Date: Thu Feb 20 09:30:13 2025 +0200 Merge branch 'main' into feat/cat-sign-doc-validator commit b7bbe15 Author: Mr-Leshiy <[email protected]> Date: Wed Feb 19 20:39:44 2025 +0200 finilize validation logic commit 40c82d2 Author: Mr-Leshiy <[email protected]> Date: Wed Feb 19 19:32:42 2025 +0200 refactor commit 249f0b4 Author: Mr-Leshiy <[email protected]> Date: Wed Feb 19 14:40:12 2025 +0200 fix signature test commit 38104bb Author: Mr-Leshiy <[email protected]> Date: Wed Feb 19 12:44:06 2025 +0200 wip commit d60579c Author: Mr-Leshiy <[email protected]> Date: Wed Feb 19 11:52:53 2025 +0200 add utils mod commit 20929de Author: Mr-Leshiy <[email protected]> Date: Wed Feb 19 11:33:25 2025 +0200 wip commit 6d933fb Author: Mr-Leshiy <[email protected]> Date: Wed Feb 19 11:13:28 2025 +0200 added new Section struct commit 09e1b2c Author: Mr-Leshiy <[email protected]> Date: Tue Feb 18 22:15:49 2025 +0200 refactor commit 7efe2e2 Author: Mr-Leshiy <[email protected]> Date: Tue Feb 18 14:55:46 2025 +0200 cleanup commit bfd27c0 Author: Mr-Leshiy <[email protected]> Date: Tue Feb 18 13:40:38 2025 +0200 add section rule commit 613a958 Author: Mr-Leshiy <[email protected]> Date: Tue Feb 18 13:31:39 2025 +0200 add more rules commit fb9e256 Author: Mr-Leshiy <[email protected]> Date: Tue Feb 18 12:37:45 2025 +0200 refactor commit 2c2bd38 Author: Mr-Leshiy <[email protected]> Date: Mon Feb 17 23:24:23 2025 +0200 fix commit 3c9506b Merge: fc8d5ba d32efec Author: Alex Pozhylenkov <[email protected]> Date: Mon Feb 17 23:00:33 2025 +0200 Merge branch 'main' into feat/cat-sign-doc-validator commit fc8d5ba Author: Mr-Leshiy <[email protected]> Date: Mon Feb 17 22:27:00 2025 +0200 make all validation async commit c287e3a Author: Mr-Leshiy <[email protected]> Date: Mon Feb 17 14:21:12 2025 +0200 make a separate trait commit 40478ae Author: Mr-Leshiy <[email protected]> Date: Mon Feb 17 14:07:47 2025 +0200 make signed doc signature validation async commit cd4301c Author: Mr-Leshiy <[email protected]> Date: Mon Feb 17 13:59:37 2025 +0200 wip commit 2182445 Author: Mr-Leshiy <[email protected]> Date: Mon Feb 17 10:19:13 2025 +0200 wip commit 6630c02 Author: Mr-Leshiy <[email protected]> Date: Sun Feb 16 21:00:14 2025 +0200 wip commit f9ec1fc Author: Mr-Leshiy <[email protected]> Date: Sun Feb 16 18:27:42 2025 +0200 replace trait with Fn commit d173bf4 Author: Mr-Leshiy <[email protected]> Date: Sun Feb 16 17:59:40 2025 +0200 wip commit 49b541e Author: Mr-Leshiy <[email protected]> Date: Sun Feb 16 16:13:56 2025 +0200 add full template validation commit d710304 Author: Mr-Leshiy <[email protected]> Date: Sun Feb 16 15:58:07 2025 +0200 wip commit b54623a Author: Mr-Leshiy <[email protected]> Date: Sun Feb 16 15:21:26 2025 +0200 refactor commit 1ce8a3c Author: Mr-Leshiy <[email protected]> Date: Sun Feb 16 14:08:35 2025 +0200 add proposal document validation commit c6f3a04 Merge: b5d70cb d26f589 Author: Mr-Leshiy <[email protected]> Date: Sun Feb 16 13:01:04 2025 +0200 Merge branch 'main' into feat/cat-sign-doc-validator commit b5d70cb Merge: 5bb6cc4 5f31d75 Author: Joaquín Rosales <[email protected]> Date: Thu Feb 13 12:50:13 2025 -0600 Merge remote-tracking branch 'origin/main' into feat/cat-sign-doc-validator commit 5bb6cc4 Author: Joaquín Rosales <[email protected]> Date: Thu Feb 13 12:49:24 2025 -0600 fix(rust/signed-doc): signed docs implement Clone commit 8b9622b Author: Joaquín Rosales <[email protected]> Date: Thu Feb 13 08:37:58 2025 -0600 chore(docs): fix spelling commit 1b0cbb5 Merge: c2a8d7b c441fb6 Author: Joaquín Rosales <[email protected]> Date: Thu Feb 13 00:35:46 2025 -0600 Merge remote-tracking branch 'origin/main' into feat/cat-sign-doc-validator commit c2a8d7b Author: Joaquín Rosales <[email protected]> Date: Thu Feb 13 00:35:33 2025 -0600 wip(rust/signed_doc): add comment document validation commit 25107e6 Merge: 7014694 9b9de32 Author: Joaquín Rosales <[email protected]> Date: Tue Feb 11 19:02:06 2025 -0600 Merge remote-tracking branch 'origin/main' into feat/cat-sign-doc-validator commit 7014694 Author: Joaquín Rosales <[email protected]> Date: Mon Feb 3 20:46:55 2025 -0600 wip(rust/signed_doc): add comment document type * refactor * refactor builder * refactor content_type validation rule * move document signature validation into the validator mod * remove uneeded 'static * fix test * fix clippy * add content_type rule and content_encoding rule tests * add ref rule test * fix * add category rule test * fix * wip * add reply tests * refactor ref * wip * wip * wip * add section rule test * wip * add template document tests * fix spelling * fix spelling * fix clippy * make pub use of `catalyst_types` imports * wip * save mk_signed_doc as an artifact * fix clippy * wip * fix clippy
1 parent 4325244 commit de7680f

21 files changed

+1407
-643
lines changed

rust/Earthfile

+2
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ build:
6767

6868
SAVE ARTIFACT target/doc doc
6969
SAVE ARTIFACT target/release/cbork cbork
70+
# Catalyst Signed Documents cli tool
71+
SAVE ARTIFACT target/release/examples/mk_signed_doc mk_signed_doc
7072

7173
# build-src-check: Check for any caching issues with the source we are building against.
7274
check-builder-src-cache:

rust/signed_doc/Cargo.toml

+1-5
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,4 @@ futures = "0.3.31"
3030
base64-url = "3.0.0"
3131
rand = "0.8.5"
3232
tokio = { version = "1.42.0", features = [ "macros" ] }
33-
ed25519-dalek = { version = "2.1.1", features = ["rand_core"] }
34-
35-
[[bin]]
36-
name = "signed-docs"
37-
path = "examples/mk_signed_doc.rs"
33+
ed25519-dalek = { version = "2.1.1", features = ["rand_core", "pem"] }

rust/signed_doc/examples/mk_signed_doc.rs

+14-9
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use std::{
99
};
1010

1111
use anyhow::Context;
12-
use catalyst_signed_doc::{Builder, CatalystSignedDocument, IdUri, Metadata};
12+
use catalyst_signed_doc::{Builder, CatalystSignedDocument, IdUri};
1313
use clap::Parser;
1414
use ed25519_dalek::pkcs8::DecodePrivateKey;
1515

@@ -59,8 +59,8 @@ impl Cli {
5959
match self {
6060
Self::Build { doc, output, meta } => {
6161
// Load Metadata from JSON file
62-
let metadata: Metadata = load_json_from_file(&meta)
63-
.map_err(|e| anyhow::anyhow!("Failed to load metadata from file: {e}"))?;
62+
let metadata: serde_json::Value =
63+
load_json_from_file(&meta).context("Failed to load metadata from file")?;
6464
println!("{metadata}");
6565
// Load Document from JSON file
6666
let json_doc: serde_json::Value = load_json_from_file(&doc)?;
@@ -69,17 +69,22 @@ impl Cli {
6969
// Start with no signatures.
7070
let signed_doc = Builder::new()
7171
.with_decoded_content(payload)
72-
.with_metadata(metadata)
73-
.build()?;
72+
.with_json_metadata(metadata)?
73+
.build();
74+
println!(
75+
"report {}",
76+
serde_json::to_string(&signed_doc.problem_report())?
77+
);
7478
save_signed_doc(signed_doc, &output)?;
7579
},
7680
Self::Sign { sk, doc, kid } => {
77-
let sk = load_secret_key_from_file(&sk)
78-
.map_err(|e| anyhow::anyhow!("Failed to load SK FILE: {e}"))?;
81+
let sk = load_secret_key_from_file(&sk).context("Failed to load SK FILE")?;
7982
let cose_bytes = read_bytes_from_file(&doc)?;
8083
let signed_doc = signed_doc_from_bytes(cose_bytes.as_slice())?;
81-
let builder = signed_doc.into_builder()?;
82-
let new_signed_doc = builder.add_signature(sk.to_bytes(), kid)?.build()?;
84+
let new_signed_doc = signed_doc
85+
.into_builder()
86+
.add_signature(sk.to_bytes(), kid)?
87+
.build();
8388
save_signed_doc(new_signed_doc, &doc)?;
8489
},
8590
Self::Inspect { path } => {

rust/signed_doc/src/builder.rs

+42-68
Original file line numberDiff line numberDiff line change
@@ -8,50 +8,43 @@ use crate::{
88
};
99

1010
/// Catalyst Signed Document Builder.
11-
#[derive(Debug, Default, Clone)]
12-
pub struct Builder {
13-
/// Document Metadata
14-
metadata: Option<Metadata>,
15-
/// Document Content
16-
content: Option<Vec<u8>>,
17-
/// Signatures
18-
signatures: Signatures,
11+
#[derive(Debug)]
12+
pub struct Builder(InnerCatalystSignedDocument);
13+
14+
impl Default for Builder {
15+
fn default() -> Self {
16+
Self::new()
17+
}
1918
}
2019

2120
impl Builder {
2221
/// Start building a signed document
2322
#[must_use]
2423
pub fn new() -> Self {
25-
Self::default()
26-
}
27-
28-
/// Set document metadata
29-
#[must_use]
30-
pub fn with_metadata(mut self, metadata: Metadata) -> Self {
31-
self.metadata = Some(metadata);
32-
self
24+
let report = ProblemReport::new(PROBLEM_REPORT_CTX);
25+
Self(InnerCatalystSignedDocument {
26+
report,
27+
metadata: Metadata::default(),
28+
content: Content::default(),
29+
signatures: Signatures::default(),
30+
})
3331
}
3432

3533
/// Set document metadata in JSON format
34+
/// Collect problem report if some fields are missing.
3635
///
3736
/// # Errors
38-
/// - Fails if it is invalid metadata JSON object.
37+
/// - Fails if it is invalid metadata fields JSON object.
3938
pub fn with_json_metadata(mut self, json: serde_json::Value) -> anyhow::Result<Self> {
40-
self.metadata = Some(serde_json::from_value(json)?);
39+
let metadata = serde_json::from_value(json)?;
40+
self.0.metadata = Metadata::from_metadata_fields(metadata, &self.0.report);
4141
Ok(self)
4242
}
4343

4444
/// Set decoded (original) document content bytes
4545
#[must_use]
4646
pub fn with_decoded_content(mut self, content: Vec<u8>) -> Self {
47-
self.content = Some(content);
48-
self
49-
}
50-
51-
/// Set document signatures
52-
#[must_use]
53-
pub fn with_signatures(mut self, signatures: Signatures) -> Self {
54-
self.signatures = signatures;
47+
self.0.content = Content::from_decoded(content);
5548
self
5649
}
5750

@@ -62,59 +55,40 @@ impl Builder {
6255
/// Fails if a `CatalystSignedDocument` cannot be created due to missing metadata or
6356
/// content, due to malformed data, or when the signed document cannot be
6457
/// converted into `coset::CoseSign`.
65-
pub fn add_signature(self, sk: SecretKey, kid: IdUri) -> anyhow::Result<Self> {
58+
pub fn add_signature(mut self, sk: SecretKey, kid: IdUri) -> anyhow::Result<Self> {
6659
let cose_sign = self
67-
.clone()
68-
.build()
69-
.map_err(|e| anyhow::anyhow!("Failed to sign: {e}"))?
60+
.0
7061
.as_cose_sign()
7162
.map_err(|e| anyhow::anyhow!("Failed to sign: {e}"))?;
72-
let Self {
73-
metadata: Some(metadata),
74-
content: Some(content),
75-
mut signatures,
76-
} = self
77-
else {
78-
anyhow::bail!("Metadata and Content are needed for signing");
79-
};
63+
8064
let sk = ed25519_dalek::SigningKey::from_bytes(&sk);
81-
let protected_header = coset::HeaderBuilder::new()
82-
.key_id(kid.to_string().into_bytes())
83-
.algorithm(metadata.algorithm()?.into());
65+
let protected_header = coset::HeaderBuilder::new().key_id(kid.to_string().into_bytes());
66+
8467
let mut signature = coset::CoseSignatureBuilder::new()
8568
.protected(protected_header.build())
8669
.build();
8770
let data_to_sign = cose_sign.tbs_data(&[], &signature);
8871
signature.signature = sk.sign(&data_to_sign).to_vec();
89-
signatures.push(kid, signature);
90-
Ok(Self::new()
91-
.with_decoded_content(content)
92-
.with_metadata(metadata)
93-
.with_signatures(signatures))
72+
self.0.signatures.push(kid, signature);
73+
74+
Ok(self)
9475
}
9576

96-
/// Build a signed document
97-
///
98-
/// ## Errors
99-
///
100-
/// Fails if any of the fields are missing.
101-
pub fn build(self) -> anyhow::Result<CatalystSignedDocument> {
102-
let Some(metadata) = self.metadata else {
103-
anyhow::bail!("Failed to build Catalyst Signed Document, missing metadata");
104-
};
105-
let Some(content) = self.content else {
106-
anyhow::bail!("Failed to build Catalyst Signed Document, missing document's content");
107-
};
108-
let signatures = self.signatures;
109-
let content = Content::from_decoded(content, metadata.content_type()?)?;
77+
/// Build a signed document with the collected error report.
78+
/// Could provide an invalid document.
79+
#[must_use]
80+
pub fn build(self) -> CatalystSignedDocument {
81+
self.0.into()
82+
}
83+
}
11084

111-
let empty_report = ProblemReport::new(PROBLEM_REPORT_CTX);
112-
Ok(InnerCatalystSignedDocument {
113-
metadata,
114-
content,
115-
signatures,
116-
report: empty_report,
117-
}
118-
.into())
85+
impl From<&CatalystSignedDocument> for Builder {
86+
fn from(value: &CatalystSignedDocument) -> Self {
87+
Self(InnerCatalystSignedDocument {
88+
metadata: value.inner.metadata.clone(),
89+
content: value.inner.content.clone(),
90+
signatures: value.inner.signatures.clone(),
91+
report: value.inner.report.clone(),
92+
})
11993
}
12094
}

rust/signed_doc/src/content.rs

+14-28
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
//! Catalyst Signed Document Content Payload
22
3+
use anyhow::Context;
34
use catalyst_types::problem_report::ProblemReport;
45

5-
use crate::metadata::{ContentEncoding, ContentType};
6+
use crate::metadata::ContentEncoding;
67

78
/// Decompressed Document Content type bytes.
89
#[derive(Debug, Clone, PartialEq, Default)]
@@ -16,8 +17,7 @@ impl Content {
1617
/// verifies a Document's content, that it is correctly encoded and it corresponds and
1718
/// parsed to the specified type
1819
pub(crate) fn from_encoded(
19-
mut data: Vec<u8>, content_type: Option<ContentType>,
20-
content_encoding: Option<ContentEncoding>, report: &ProblemReport,
20+
mut data: Vec<u8>, content_encoding: Option<ContentEncoding>, report: &ProblemReport,
2121
) -> Self {
2222
if let Some(content_encoding) = content_encoding {
2323
if let Ok(decoded_data) = content_encoding.decode(&data) {
@@ -32,29 +32,12 @@ impl Content {
3232
return Self::default();
3333
}
3434
}
35-
if let Some(content_type) = content_type {
36-
if content_type.validate(&data).is_err() {
37-
report.invalid_value(
38-
"payload",
39-
&hex::encode(&data),
40-
&format!("Invalid Document content type, should {content_type} encodable"),
41-
"Invalid Document content type.",
42-
);
43-
return Self::default();
44-
}
45-
}
46-
47-
Self { data: Some(data) }
35+
Self::from_decoded(data)
4836
}
4937

5038
/// Creates a new `Content` value, from the decoded (original) data.
51-
/// verifies that it corresponds and parsed to the specified type.
52-
///
53-
/// # Errors
54-
/// Returns an error if content is not correctly encoded
55-
pub(crate) fn from_decoded(data: Vec<u8>, content_type: ContentType) -> anyhow::Result<Self> {
56-
content_type.validate(&data)?;
57-
Ok(Self { data: Some(data) })
39+
pub(crate) fn from_decoded(data: Vec<u8>) -> Self {
40+
Self { data: Some(data) }
5841
}
5942

6043
/// Return an decoded (original) content bytes.
@@ -74,13 +57,16 @@ impl Content {
7457
/// - Missing Document content
7558
/// - Failed to encode content.
7659
pub(crate) fn encoded_bytes(
77-
&self, content_encoding: ContentEncoding,
60+
&self, content_encoding: Option<ContentEncoding>,
7861
) -> anyhow::Result<Vec<u8>> {
7962
let content = self.decoded_bytes()?;
80-
let data = content_encoding
81-
.encode(content)
82-
.map_err(|e| anyhow::anyhow!("Failed to encode {content_encoding} content: {e}"))?;
83-
Ok(data)
63+
if let Some(content_encoding) = content_encoding {
64+
content_encoding
65+
.encode(content)
66+
.context(format!("Failed to encode {content_encoding} content"))
67+
} else {
68+
Ok(content.to_vec())
69+
}
8470
}
8571

8672
/// Return content byte size.

0 commit comments

Comments
 (0)