Skip to content

Commit 3dc3c11

Browse files
Mr-Leshiyapskhem
andauthored
feat(rust/signed-doc): Catalyst Signed Document id and ver fields timestamp validation (#265)
* add id and ver timebased validation * fix tests * add test * fix spelling * fix clippy * Update rust/signed_doc/src/validator/mod.rs Co-authored-by: Apisit Ritruengroj <[email protected]> * wip * wip --------- Co-authored-by: Apisit Ritruengroj <[email protected]>
1 parent ad6828b commit 3dc3c11

File tree

6 files changed

+218
-30
lines changed

6 files changed

+218
-30
lines changed

rust/signed_doc/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ futures = "0.3.31"
3030
[dev-dependencies]
3131
base64-url = "3.0.0"
3232
rand = "0.8.5"
33+
uuid = { version = "1.12.0", features = ["v7"] }
3334
tokio = { version = "1.42.0", features = [ "macros" ] }
3435

3536
[[bin]]

rust/signed_doc/src/metadata/mod.rs

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -126,18 +126,6 @@ impl Metadata {
126126
);
127127
}
128128

129-
if let Some(id) = metadata.id {
130-
if let Some(ver) = metadata.ver {
131-
if ver < id {
132-
report.invalid_value(
133-
"ver",
134-
&ver.to_string(),
135-
"ver < id",
136-
&format!("Document Version {ver} cannot be smaller than Document ID {id}"),
137-
);
138-
}
139-
}
140-
}
141129
Self(metadata)
142130
}
143131

rust/signed_doc/src/validator/mod.rs

Lines changed: 175 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
pub(crate) mod rules;
44
pub(crate) mod utils;
55

6-
use std::{collections::HashMap, sync::LazyLock};
6+
use std::{collections::HashMap, sync::LazyLock, time::SystemTime};
77

88
use catalyst_types::{id_uri::IdUri, problem_report::ProblemReport, uuid::Uuid};
99
use coset::{CoseSign, CoseSignature};
@@ -108,12 +108,14 @@ fn document_rules_init() -> HashMap<Uuid, Rules> {
108108
}
109109

110110
/// A comprehensive document type based validation of the `CatalystSignedDocument`.
111-
/// Return true if all signatures are valid, otherwise return false.
111+
/// Includes time based validation of the `id` and `ver` fields based on the provided
112+
/// `future_threshold` and `past_threshold` threshold values (in seconds).
113+
/// Return true if it is valid, otherwise return false.
112114
///
113115
/// # Errors
114116
/// If `provider` returns error, fails fast throwing that error.
115117
pub async fn validate<Provider>(
116-
doc: &CatalystSignedDocument, provider: &Provider,
118+
doc: &CatalystSignedDocument, future_threshold: u64, past_threshold: u64, provider: &Provider,
117119
) -> anyhow::Result<bool>
118120
where Provider: CatalystSignedDocumentProvider {
119121
let Ok(doc_type) = doc.doc_type() else {
@@ -124,6 +126,10 @@ where Provider: CatalystSignedDocumentProvider {
124126
return Ok(false);
125127
};
126128

129+
if !validate_id_and_ver(doc, future_threshold, past_threshold)? {
130+
return Ok(false);
131+
}
132+
127133
let Some(rules) = DOCUMENT_RULES.get(&doc_type.uuid()) else {
128134
doc.report().invalid_value(
129135
"`type`",
@@ -136,6 +142,82 @@ where Provider: CatalystSignedDocumentProvider {
136142
rules.check(doc, provider).await
137143
}
138144

145+
/// Validates document `id` and `ver` fields on the timestamps:
146+
/// 1. document `ver` cannot be smaller than document id field
147+
/// 2. document `id` cannot be too far in the future (`future_threshold` arg) from
148+
/// `SystemTime::now()` based on the provide threshold
149+
/// 3. document `id` cannot be too far behind (`past_threshold` arg) from
150+
/// `SystemTime::now()` based on the provide threshold
151+
fn validate_id_and_ver(
152+
doc: &CatalystSignedDocument, future_threshold: u64, past_threshold: u64,
153+
) -> anyhow::Result<bool> {
154+
let id = doc.doc_id().ok();
155+
let ver = doc.doc_ver().ok();
156+
if id.is_none() {
157+
doc.report().missing_field(
158+
"id",
159+
"Can't get a document id during the validation process",
160+
);
161+
}
162+
if ver.is_none() {
163+
doc.report().missing_field(
164+
"ver",
165+
"Can't get a document ver during the validation process",
166+
);
167+
}
168+
match (id, ver) {
169+
(Some(id), Some(ver)) => {
170+
let mut is_valid = true;
171+
if ver < id {
172+
doc.report().invalid_value(
173+
"ver",
174+
&ver.to_string(),
175+
"ver < id",
176+
&format!("Document Version {ver} cannot be smaller than Document ID {id}"),
177+
);
178+
is_valid = false;
179+
}
180+
181+
let (id_time, _) = id
182+
.uuid()
183+
.get_timestamp()
184+
.ok_or(anyhow::anyhow!("Document id field must be a UUIDv7"))?
185+
.to_unix();
186+
187+
let now = SystemTime::now()
188+
.duration_since(SystemTime::UNIX_EPOCH)
189+
.map_err(|_| {
190+
anyhow::anyhow!(
191+
"Cannot validate document id field, SystemTime before UNIX EPOCH!"
192+
)
193+
})?
194+
.as_secs();
195+
196+
if id_time > now.saturating_add(future_threshold) {
197+
doc.report().invalid_value(
198+
"id",
199+
&ver.to_string(),
200+
"id < now + future_threshold",
201+
&format!("Document ID timestamp {id} cannot be too far in future (threshold: {future_threshold}) from now: {now}"),
202+
);
203+
is_valid = false;
204+
}
205+
if id_time < now.saturating_sub(past_threshold) {
206+
doc.report().invalid_value(
207+
"id",
208+
&ver.to_string(),
209+
"id > now - past_threshold",
210+
&format!("Document ID timestamp {id} cannot be too far behind (threshold: {past_threshold}) from now: {now}"),
211+
);
212+
is_valid = false;
213+
}
214+
Ok(is_valid)
215+
},
216+
217+
_ => Ok(false),
218+
}
219+
}
220+
139221
/// Verify document signatures.
140222
/// Return true if all signatures are valid, otherwise return false.
141223
///
@@ -208,12 +290,99 @@ where
208290
Ok(true)
209291
}
210292

211-
#[cfg(test)]
212-
mod tests {
213-
use super::*;
293+
#[allow(missing_docs)]
294+
pub mod tests {
295+
/// A Test Future Threshold value for the Document's time based id field validation (5
296+
/// secs);
297+
pub const TEST_FUTURE_THRESHOLD: u64 = 5;
298+
/// A Test Future Threshold value for the Document's time based id field validation (5
299+
/// secs);
300+
pub const TEST_PAST_THRESHOLD: u64 = 5;
214301

302+
#[cfg(test)]
303+
#[test]
304+
fn document_id_and_ver_test() {
305+
use std::time::SystemTime;
306+
307+
use uuid::{Timestamp, Uuid};
308+
309+
use crate::{validator::validate_id_and_ver, Builder, UuidV7};
310+
311+
let now = SystemTime::now()
312+
.duration_since(SystemTime::UNIX_EPOCH)
313+
.unwrap()
314+
.as_secs();
315+
316+
let uuid_v7 = UuidV7::new();
317+
let doc = Builder::new()
318+
.with_json_metadata(serde_json::json!({
319+
"id": uuid_v7.to_string(),
320+
"ver": uuid_v7.to_string()
321+
}))
322+
.unwrap()
323+
.build();
324+
325+
let is_valid =
326+
validate_id_and_ver(&doc, TEST_FUTURE_THRESHOLD, TEST_PAST_THRESHOLD).unwrap();
327+
assert!(is_valid);
328+
329+
let ver = Uuid::new_v7(Timestamp::from_unix_time(now - 1, 0, 0, 0));
330+
let id = Uuid::new_v7(Timestamp::from_unix_time(now + 1, 0, 0, 0));
331+
assert!(ver < id);
332+
let doc = Builder::new()
333+
.with_json_metadata(serde_json::json!({
334+
"id": id.to_string(),
335+
"ver": ver.to_string()
336+
}))
337+
.unwrap()
338+
.build();
339+
340+
let is_valid =
341+
validate_id_and_ver(&doc, TEST_FUTURE_THRESHOLD, TEST_PAST_THRESHOLD).unwrap();
342+
assert!(!is_valid);
343+
344+
let to_far_in_past = Uuid::new_v7(Timestamp::from_unix_time(
345+
now - TEST_PAST_THRESHOLD - 1,
346+
0,
347+
0,
348+
0,
349+
));
350+
let doc = Builder::new()
351+
.with_json_metadata(serde_json::json!({
352+
"id": to_far_in_past.to_string(),
353+
"ver": to_far_in_past.to_string()
354+
}))
355+
.unwrap()
356+
.build();
357+
358+
let is_valid =
359+
validate_id_and_ver(&doc, TEST_FUTURE_THRESHOLD, TEST_PAST_THRESHOLD).unwrap();
360+
assert!(!is_valid);
361+
362+
let to_far_in_future = Uuid::new_v7(Timestamp::from_unix_time(
363+
now + TEST_FUTURE_THRESHOLD + 1,
364+
0,
365+
0,
366+
0,
367+
));
368+
let doc = Builder::new()
369+
.with_json_metadata(serde_json::json!({
370+
"id": to_far_in_future.to_string(),
371+
"ver": to_far_in_future.to_string()
372+
}))
373+
.unwrap()
374+
.build();
375+
376+
let is_valid =
377+
validate_id_and_ver(&doc, TEST_FUTURE_THRESHOLD, TEST_PAST_THRESHOLD).unwrap();
378+
assert!(!is_valid);
379+
}
380+
381+
#[cfg(test)]
215382
#[test]
216383
fn document_rules_init_test() {
384+
use super::document_rules_init;
385+
217386
document_rules_init();
218387
}
219388
}

rust/signed_doc/tests/comment.rs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
//! Integration test for comment document validation part.
22
3-
use catalyst_signed_doc::{providers::tests::TestCatalystSignedDocumentProvider, *};
3+
use catalyst_signed_doc::{
4+
providers::tests::TestCatalystSignedDocumentProvider,
5+
validator::tests::{TEST_FUTURE_THRESHOLD, TEST_PAST_THRESHOLD},
6+
*,
7+
};
48

59
mod common;
610

@@ -31,7 +35,9 @@ async fn test_valid_comment_doc() {
3135
provider.add_document(template_doc).unwrap();
3236
provider.add_document(proposal_doc).unwrap();
3337

34-
let is_valid = validator::validate(&doc, &provider).await.unwrap();
38+
let is_valid = validator::validate(&doc, TEST_FUTURE_THRESHOLD, TEST_PAST_THRESHOLD, &provider)
39+
.await
40+
.unwrap();
3541

3642
assert!(is_valid);
3743
}
@@ -85,7 +91,9 @@ async fn test_valid_comment_doc_with_reply() {
8591
provider.add_document(proposal_doc).unwrap();
8692
provider.add_document(comment_doc).unwrap();
8793

88-
let is_valid = validator::validate(&doc, &provider).await.unwrap();
94+
let is_valid = validator::validate(&doc, TEST_FUTURE_THRESHOLD, TEST_PAST_THRESHOLD, &provider)
95+
.await
96+
.unwrap();
8997

9098
assert!(is_valid);
9199
}
@@ -116,7 +124,9 @@ async fn test_invalid_comment_doc() {
116124
provider.add_document(template_doc).unwrap();
117125
provider.add_document(proposal_doc).unwrap();
118126

119-
let is_valid = validator::validate(&doc, &provider).await.unwrap();
127+
let is_valid = validator::validate(&doc, TEST_FUTURE_THRESHOLD, TEST_PAST_THRESHOLD, &provider)
128+
.await
129+
.unwrap();
120130

121131
assert!(!is_valid);
122132
}

rust/signed_doc/tests/proposal.rs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
//! Integration test for proposal document validation part.
22
3-
use catalyst_signed_doc::{providers::tests::TestCatalystSignedDocumentProvider, *};
3+
use catalyst_signed_doc::{
4+
providers::tests::TestCatalystSignedDocumentProvider,
5+
validator::tests::{TEST_FUTURE_THRESHOLD, TEST_PAST_THRESHOLD},
6+
*,
7+
};
48

59
mod common;
610

@@ -25,7 +29,9 @@ async fn test_valid_proposal_doc() {
2529
let mut provider = TestCatalystSignedDocumentProvider::default();
2630
provider.add_document(template_doc).unwrap();
2731

28-
let is_valid = validator::validate(&doc, &provider).await.unwrap();
32+
let is_valid = validator::validate(&doc, TEST_FUTURE_THRESHOLD, TEST_PAST_THRESHOLD, &provider)
33+
.await
34+
.unwrap();
2935

3036
assert!(is_valid);
3137
}
@@ -50,7 +56,9 @@ async fn test_valid_proposal_doc_with_empty_provider() {
5056

5157
let provider = TestCatalystSignedDocumentProvider::default();
5258

53-
let is_valid = validator::validate(&doc, &provider).await.unwrap();
59+
let is_valid = validator::validate(&doc, TEST_FUTURE_THRESHOLD, TEST_PAST_THRESHOLD, &provider)
60+
.await
61+
.unwrap();
5462

5563
assert!(!is_valid);
5664
}
@@ -71,7 +79,9 @@ async fn test_invalid_proposal_doc() {
7179

7280
let provider = TestCatalystSignedDocumentProvider::default();
7381

74-
let is_valid = validator::validate(&doc, &provider).await.unwrap();
82+
let is_valid = validator::validate(&doc, TEST_FUTURE_THRESHOLD, TEST_PAST_THRESHOLD, &provider)
83+
.await
84+
.unwrap();
7585

7686
assert!(!is_valid);
7787
}

rust/signed_doc/tests/submission.rs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
//! Test for proposal submission action.
22
3-
use catalyst_signed_doc::{providers::tests::TestCatalystSignedDocumentProvider, *};
3+
use catalyst_signed_doc::{
4+
providers::tests::TestCatalystSignedDocumentProvider,
5+
validator::tests::{TEST_FUTURE_THRESHOLD, TEST_PAST_THRESHOLD},
6+
*,
7+
};
48

59
mod common;
610

@@ -25,7 +29,9 @@ async fn test_valid_submission_action() {
2529
let mut provider = TestCatalystSignedDocumentProvider::default();
2630
provider.add_document(proposal_doc).unwrap();
2731

28-
let is_valid = validator::validate(&doc, &provider).await.unwrap();
32+
let is_valid = validator::validate(&doc, TEST_FUTURE_THRESHOLD, TEST_PAST_THRESHOLD, &provider)
33+
.await
34+
.unwrap();
2935

3036
assert!(is_valid);
3137
}
@@ -49,7 +55,9 @@ async fn test_valid_submission_action_with_empty_provider() {
4955

5056
let provider = TestCatalystSignedDocumentProvider::default();
5157

52-
let is_valid = validator::validate(&doc, &provider).await.unwrap();
58+
let is_valid = validator::validate(&doc, TEST_FUTURE_THRESHOLD, TEST_PAST_THRESHOLD, &provider)
59+
.await
60+
.unwrap();
5361

5462
assert!(!is_valid);
5563
}
@@ -70,7 +78,9 @@ async fn test_invalid_submission_action() {
7078

7179
let provider = TestCatalystSignedDocumentProvider::default();
7280

73-
let is_valid = validator::validate(&doc, &provider).await.unwrap();
81+
let is_valid = validator::validate(&doc, TEST_FUTURE_THRESHOLD, TEST_PAST_THRESHOLD, &provider)
82+
.await
83+
.unwrap();
7484

7585
assert!(!is_valid);
7686
}

0 commit comments

Comments
 (0)