33pub ( crate ) mod rules;
44pub ( crate ) mod utils;
55
6- use std:: { collections:: HashMap , sync:: LazyLock } ;
6+ use std:: { collections:: HashMap , sync:: LazyLock , time :: SystemTime } ;
77
88use catalyst_types:: { id_uri:: IdUri , problem_report:: ProblemReport , uuid:: Uuid } ;
99use 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.
115117pub async fn validate < Provider > (
116- doc : & CatalystSignedDocument , provider : & Provider ,
118+ doc : & CatalystSignedDocument , future_threshold : u64 , past_threshold : u64 , provider : & Provider ,
117119) -> anyhow:: Result < bool >
118120where 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}
0 commit comments