Skip to content

Commit 4325244

Browse files
feat(rust/signed-doc): Comment and Proposal Document Validation (#207)
* wip(rust/signed_doc): add comment document type * wip(rust/signed_doc): add comment document validation * chore(docs): fix spelling * fix(rust/signed-doc): signed docs implement Clone * add proposal document validation * refactor * wip * add full template validation * wip * replace trait with Fn * wip * wip * wip * make signed doc signature validation async * make a separate trait * make all validation async * fix * refactor * add more rules * add section rule * cleanup * refactor * added new Section struct * wip * add utils mod * wip * fix signature test * refactor * finilize validation logic * fix spelling * fix clippy lints * fix --------- Co-authored-by: Mr-Leshiy <[email protected]>
1 parent ca010d7 commit 4325244

30 files changed

+1341
-874
lines changed

rust/signed_doc/Cargo.toml

+7-5
Original file line numberDiff line numberDiff line change
@@ -11,24 +11,26 @@ license.workspace = true
1111
workspace = true
1212

1313
[dependencies]
14-
rbac-registration = { version = "0.0.4", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250220-00" }
1514
catalyst-types = { version = "0.0.3", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250220-00" }
1615
anyhow = "1.0.95"
1716
serde = { version = "1.0.217", features = ["derive"] }
1817
serde_json = "1.0.134"
1918
coset = "0.3.8"
2019
minicbor = { version = "0.25.1", features = ["half"] }
2120
brotli = "7.0.0"
22-
ed25519-dalek = { version = "2.1.1", features = ["pem", "rand_core"] }
21+
ed25519-dalek = { version = "2.1.1" }
2322
hex = "0.4.3"
2423
strum = { version = "0.26.3", features = ["derive"] }
25-
clap = { version = "4.5.23", features = ["derive", "env"] }
26-
24+
clap = { version = "4.5.23", features = ["derive", "env"] }
25+
jsonschema = "0.28.3"
26+
jsonpath-rust = "0.7.5"
27+
futures = "0.3.31"
2728

2829
[dev-dependencies]
2930
base64-url = "3.0.0"
3031
rand = "0.8.5"
31-
32+
tokio = { version = "1.42.0", features = [ "macros" ] }
33+
ed25519-dalek = { version = "2.1.1", features = ["rand_core"] }
3234

3335
[[bin]]
3436
name = "signed-docs"

rust/signed_doc/examples/mk_signed_doc.rs

+7-40
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@ use std::{
88
path::PathBuf,
99
};
1010

11-
use catalyst_signed_doc::{Builder, CatalystSignedDocument, IdUri, Metadata, SimplePublicKeyType};
11+
use anyhow::Context;
12+
use catalyst_signed_doc::{Builder, CatalystSignedDocument, IdUri, Metadata};
1213
use clap::Parser;
13-
use ed25519_dalek::pkcs8::{DecodePrivateKey, DecodePublicKey};
14+
use ed25519_dalek::pkcs8::DecodePrivateKey;
1415

1516
fn main() {
1617
if let Err(err) = Cli::parse().exec() {
@@ -51,16 +52,6 @@ enum Cli {
5152
/// Hex-formatted COSE SIGN Bytes
5253
cose_sign_hex: String,
5354
},
54-
/// Validates a signature by Key ID and verifying key
55-
Verify {
56-
/// Path to the formed (could be empty, without any signatures) COSE document
57-
/// This exact file would be modified and new signature would be added
58-
path: PathBuf,
59-
/// Path to the verifying key in PEM format
60-
pk: PathBuf,
61-
/// Signer kid
62-
kid: IdUri,
63-
},
6455
}
6556

6657
impl Cli {
@@ -87,7 +78,7 @@ impl Cli {
8778
.map_err(|e| anyhow::anyhow!("Failed to load SK FILE: {e}"))?;
8879
let cose_bytes = read_bytes_from_file(&doc)?;
8980
let signed_doc = signed_doc_from_bytes(cose_bytes.as_slice())?;
90-
let builder = signed_doc.into_builder();
81+
let builder = signed_doc.into_builder()?;
9182
let new_signed_doc = builder.add_signature(sk.to_bytes(), kid)?.build()?;
9283
save_signed_doc(new_signed_doc, &doc)?;
9384
},
@@ -99,22 +90,6 @@ impl Cli {
9990
let cose_bytes = hex::decode(&cose_sign_hex)?;
10091
inspect_signed_doc(&cose_bytes)?;
10192
},
102-
Self::Verify { path, pk, kid } => {
103-
let pk = load_public_key_from_file(&pk)
104-
.map_err(|e| anyhow::anyhow!("Failed to load PK FILE {pk:?}: {e}"))?;
105-
let cose_bytes = read_bytes_from_file(&path)?;
106-
let signed_doc = signed_doc_from_bytes(cose_bytes.as_slice())?;
107-
signed_doc
108-
.verify(|k| {
109-
if k.to_string() == kid.to_string() {
110-
SimplePublicKeyType::Ed25519(pk)
111-
} else {
112-
SimplePublicKeyType::Undefined
113-
}
114-
})
115-
.map_err(|e| anyhow::anyhow!("Catalyst Document Verification failed: {e}"))?;
116-
println!("Catalyst Signed Document is Verified.");
117-
},
11893
}
11994
println!("Done");
12095
Ok(())
@@ -142,15 +117,13 @@ fn inspect_signed_doc(cose_bytes: &[u8]) -> anyhow::Result<()> {
142117

143118
fn save_signed_doc(signed_doc: CatalystSignedDocument, path: &PathBuf) -> anyhow::Result<()> {
144119
let mut bytes: Vec<u8> = Vec::new();
145-
minicbor::encode(signed_doc, &mut bytes)
146-
.map_err(|e| anyhow::anyhow!("Failed to encode document: {e}"))?;
120+
minicbor::encode(signed_doc, &mut bytes).context("Failed to encode document")?;
147121

148122
write_bytes_to_file(&bytes, path)
149123
}
150124

151125
fn signed_doc_from_bytes(cose_bytes: &[u8]) -> anyhow::Result<CatalystSignedDocument> {
152-
CatalystSignedDocument::try_from(cose_bytes)
153-
.map_err(|e| anyhow::anyhow!("Invalid Catalyst Document: {e}"))
126+
minicbor::decode(cose_bytes).context("Invalid Catalyst Document")
154127
}
155128

156129
fn load_json_from_file<T>(path: &PathBuf) -> anyhow::Result<T>
@@ -163,17 +136,11 @@ where T: for<'de> serde::Deserialize<'de> {
163136
fn write_bytes_to_file(bytes: &[u8], output: &PathBuf) -> anyhow::Result<()> {
164137
File::create(output)?
165138
.write_all(bytes)
166-
.map_err(|e| anyhow::anyhow!("Failed to write to file {output:?}: {e}"))
139+
.context(format!("Failed to write to file {output:?}"))
167140
}
168141

169142
fn load_secret_key_from_file(sk_path: &PathBuf) -> anyhow::Result<ed25519_dalek::SigningKey> {
170143
let sk_str = read_to_string(sk_path)?;
171144
let sk = ed25519_dalek::SigningKey::from_pkcs8_pem(&sk_str)?;
172145
Ok(sk)
173146
}
174-
175-
fn load_public_key_from_file(pk_path: &PathBuf) -> anyhow::Result<ed25519_dalek::VerifyingKey> {
176-
let pk_str = read_to_string(pk_path)?;
177-
let pk = ed25519_dalek::VerifyingKey::from_public_key_pem(&pk_str)?;
178-
Ok(pk)
179-
}

rust/signed_doc/src/builder.rs

+18-9
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
//! Catalyst Signed Document Builder.
2-
use catalyst_types::id_uri::IdUri;
2+
use catalyst_types::{id_uri::IdUri, problem_report::ProblemReport};
33
use ed25519_dalek::{ed25519::signature::Signer, SecretKey};
44

5-
use crate::{CatalystSignedDocument, Content, InnerCatalystSignedDocument, Metadata, Signatures};
5+
use crate::{
6+
CatalystSignedDocument, Content, InnerCatalystSignedDocument, Metadata, Signatures,
7+
PROBLEM_REPORT_CTX,
8+
};
69

710
/// Catalyst Signed Document Builder.
811
#[derive(Debug, Default, Clone)]
@@ -29,6 +32,15 @@ impl Builder {
2932
self
3033
}
3134

35+
/// Set document metadata in JSON format
36+
///
37+
/// # Errors
38+
/// - Fails if it is invalid metadata JSON object.
39+
pub fn with_json_metadata(mut self, json: serde_json::Value) -> anyhow::Result<Self> {
40+
self.metadata = Some(serde_json::from_value(json)?);
41+
Ok(self)
42+
}
43+
3244
/// Set decoded (original) document content bytes
3345
#[must_use]
3446
pub fn with_decoded_content(mut self, content: Vec<u8>) -> Self {
@@ -68,7 +80,7 @@ impl Builder {
6880
let sk = ed25519_dalek::SigningKey::from_bytes(&sk);
6981
let protected_header = coset::HeaderBuilder::new()
7082
.key_id(kid.to_string().into_bytes())
71-
.algorithm(metadata.algorithm().into());
83+
.algorithm(metadata.algorithm()?.into());
7284
let mut signature = coset::CoseSignatureBuilder::new()
7385
.protected(protected_header.build())
7486
.build();
@@ -94,17 +106,14 @@ impl Builder {
94106
anyhow::bail!("Failed to build Catalyst Signed Document, missing document's content");
95107
};
96108
let signatures = self.signatures;
109+
let content = Content::from_decoded(content, metadata.content_type()?)?;
97110

98-
let content = Content::from_decoded(
99-
content,
100-
metadata.content_type(),
101-
metadata.content_encoding(),
102-
)?;
103-
111+
let empty_report = ProblemReport::new(PROBLEM_REPORT_CTX);
104112
Ok(InnerCatalystSignedDocument {
105113
metadata,
106114
content,
107115
signatures,
116+
report: empty_report,
108117
}
109118
.into())
110119
}

rust/signed_doc/src/content.rs

+58-59
Original file line numberDiff line numberDiff line change
@@ -1,93 +1,92 @@
11
//! Catalyst Signed Document Content Payload
22
3+
use catalyst_types::problem_report::ProblemReport;
4+
35
use crate::metadata::{ContentEncoding, ContentType};
46

57
/// Decompressed Document Content type bytes.
6-
#[derive(Debug, Clone, PartialEq)]
8+
#[derive(Debug, Clone, PartialEq, Default)]
79
pub struct Content {
8-
/// Content data bytes
9-
data: Vec<u8>,
10-
/// Content type
11-
content_type: ContentType,
12-
/// Content encoding
13-
content_encoding: Option<ContentEncoding>,
10+
/// Original Decompressed Document's data bytes
11+
data: Option<Vec<u8>>,
1412
}
1513

1614
impl Content {
1715
/// Creates a new `Content` value, from the encoded data.
1816
/// verifies a Document's content, that it is correctly encoded and it corresponds and
1917
/// parsed to the specified type
20-
///
21-
/// # Errors
22-
/// Returns an error if content is not correctly encoded
2318
pub(crate) fn from_encoded(
24-
mut data: Vec<u8>, content_type: ContentType, content_encoding: Option<ContentEncoding>,
25-
) -> anyhow::Result<Self> {
26-
if let Some(encoding) = content_encoding {
27-
data = encoding
28-
.decode(&data)
29-
.map_err(|e| anyhow::anyhow!("Failed to decode {encoding} content: {e}"))?;
19+
mut data: Vec<u8>, content_type: Option<ContentType>,
20+
content_encoding: Option<ContentEncoding>, report: &ProblemReport,
21+
) -> Self {
22+
if let Some(content_encoding) = content_encoding {
23+
if let Ok(decoded_data) = content_encoding.decode(&data) {
24+
data = decoded_data;
25+
} else {
26+
report.invalid_value(
27+
"payload",
28+
&hex::encode(&data),
29+
&format!("Invalid Document content, should {content_encoding} encodable"),
30+
"Invalid Document content type.",
31+
);
32+
return Self::default();
33+
}
34+
}
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+
}
3045
}
31-
content_type.validate(&data)?;
3246

33-
Ok(Self {
34-
data,
35-
content_type,
36-
content_encoding,
37-
})
47+
Self { data: Some(data) }
3848
}
3949

4050
/// Creates a new `Content` value, from the decoded (original) data.
4151
/// verifies that it corresponds and parsed to the specified type.
4252
///
4353
/// # Errors
4454
/// Returns an error if content is not correctly encoded
45-
pub(crate) fn from_decoded(
46-
data: Vec<u8>, content_type: ContentType, content_encoding: Option<ContentEncoding>,
47-
) -> anyhow::Result<Self> {
55+
pub(crate) fn from_decoded(data: Vec<u8>, content_type: ContentType) -> anyhow::Result<Self> {
4856
content_type.validate(&data)?;
49-
Ok(Self {
50-
data,
51-
content_type,
52-
content_encoding,
53-
})
54-
}
55-
56-
/// Return `true` if Document's content type is Json
57-
#[must_use]
58-
pub fn is_json(&self) -> bool {
59-
matches!(self.content_type, ContentType::Json)
57+
Ok(Self { data: Some(data) })
6058
}
6159

62-
/// Return `true` if Document's content type is Json
63-
#[must_use]
64-
pub fn is_cbor(&self) -> bool {
65-
matches!(self.content_type, ContentType::Cbor)
66-
}
67-
68-
/// Return an decoded (original) content bytes,
69-
/// by the corresponding `content_encoding` provided field.
70-
#[must_use]
71-
pub fn decoded_bytes(&self) -> &[u8] {
72-
&self.data
60+
/// Return an decoded (original) content bytes.
61+
///
62+
/// # Errors
63+
/// - Missing Document content
64+
pub fn decoded_bytes(&self) -> anyhow::Result<&[u8]> {
65+
self.data
66+
.as_deref()
67+
.ok_or(anyhow::anyhow!("Missing Document content"))
7368
}
7469

7570
/// Return an encoded content bytes,
76-
/// by the corresponding `content_encoding` provided field
77-
pub(crate) fn encoded_bytes(&self) -> anyhow::Result<Vec<u8>> {
78-
if let Some(encoding) = self.content_encoding {
79-
let data = encoding
80-
.encode(&self.data)
81-
.map_err(|e| anyhow::anyhow!("Failed to encode {encoding} content: {e}"))?;
82-
Ok(data)
83-
} else {
84-
Ok(self.data.clone())
85-
}
71+
/// by the provided `content_encoding` provided field.
72+
///
73+
/// # Errors
74+
/// - Missing Document content
75+
/// - Failed to encode content.
76+
pub(crate) fn encoded_bytes(
77+
&self, content_encoding: ContentEncoding,
78+
) -> anyhow::Result<Vec<u8>> {
79+
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)
8684
}
8785

88-
/// Return content byte size
86+
/// Return content byte size.
87+
/// If content is empty returns `0`.
8988
#[must_use]
9089
pub fn size(&self) -> usize {
91-
self.data.len()
90+
self.data.as_ref().map(Vec::len).unwrap_or_default()
9291
}
9392
}

0 commit comments

Comments
 (0)