@@ -10,17 +10,12 @@ use coset::{CborSerializable, TaggedCborSerializable};
1010
1111mod metadata;
1212
13- pub use metadata:: { DocumentRef , Metadata } ;
13+ pub use metadata:: { DocumentRef , Metadata , UuidV7 } ;
1414
1515/// Catalyst Signed Document Content Encoding Key.
1616const CONTENT_ENCODING_KEY : & str = "content encoding" ;
1717/// Catalyst Signed Document Content Encoding Value.
1818const CONTENT_ENCODING_VALUE : & str = "br" ;
19- /// CBOR tag for UUID content.
20- const UUID_CBOR_TAG : u64 = 37 ;
21-
22- /// Collection of Content Errors.
23- pub struct ContentErrors ( Vec < String > ) ;
2419
2520/// Keep all the contents private.
2621/// Better even to use a structure like this. Wrapping in an Arc means we don't have to
@@ -75,8 +70,26 @@ impl TryFrom<Vec<u8>> for CatalystSignedDocument {
7570 let cose = coset:: CoseSign :: from_tagged_slice ( & cose_bytes)
7671 . or ( coset:: CoseSign :: from_slice ( & cose_bytes) )
7772 . map_err ( |e| anyhow:: anyhow!( "Invalid COSE Sign document: {e}" ) ) ?;
73+ let mut content_errors = Vec :: new ( ) ;
74+ let expected_header = cose_protected_header ( ) ;
75+
76+ if cose. protected . header . content_type != expected_header. content_type {
77+ content_errors
78+ . push ( "Invalid COSE document protected header `content-type` field" . to_string ( ) ) ;
79+ }
7880
79- let ( metadata, content_errors) = metadata_from_cose_protected_header ( & cose) ;
81+ if !cose. protected . header . rest . iter ( ) . any ( |( key, value) | {
82+ key == & coset:: Label :: Text ( CONTENT_ENCODING_KEY . to_string ( ) )
83+ && value == & coset:: cbor:: Value :: Text ( CONTENT_ENCODING_VALUE . to_string ( ) )
84+ } ) {
85+ content_errors. push (
86+ "Invalid COSE document protected header {CONTENT_ENCODING_KEY} field" . to_string ( ) ,
87+ ) ;
88+ }
89+ let metadata = Metadata :: from ( & cose. protected ) ;
90+ if metadata. has_error ( ) {
91+ content_errors. extend_from_slice ( metadata. content_errors ( ) ) ;
92+ }
8093 let payload = match & cose. payload {
8194 Some ( payload) => {
8295 let mut buf = Vec :: new ( ) ;
@@ -95,7 +108,7 @@ impl TryFrom<Vec<u8>> for CatalystSignedDocument {
95108 payload,
96109 signatures,
97110 cose_sign : cose,
98- content_errors : content_errors . 0 ,
111+ content_errors,
99112 } ;
100113 Ok ( CatalystSignedDocument {
101114 inner : Arc :: new ( inner) ,
@@ -106,7 +119,7 @@ impl TryFrom<Vec<u8>> for CatalystSignedDocument {
106119impl CatalystSignedDocument {
107120 // A bunch of getters to access the contents, or reason through the document, such as.
108121
109- /// Are there any validation errors (as opposed to structural errors.
122+ /// Are there any validation errors (as opposed to structural errors) .
110123 #[ must_use]
111124 pub fn has_error ( & self ) -> bool {
112125 !self . inner . content_errors . is_empty ( )
@@ -166,190 +179,14 @@ fn cose_protected_header() -> coset::Header {
166179 . build ( )
167180}
168181
169- /// Decode `CBOR` encoded `UUID`.
170- fn decode_cbor_uuid ( val : & coset:: cbor:: Value ) -> anyhow:: Result < uuid:: Uuid > {
171- let Some ( ( UUID_CBOR_TAG , coset:: cbor:: Value :: Bytes ( bytes) ) ) = val. as_tag ( ) else {
172- anyhow:: bail!( "Invalid CBOR encoded UUID type" ) ;
173- } ;
174- let uuid = uuid:: Uuid :: from_bytes (
175- bytes
176- . clone ( )
177- . try_into ( )
178- . map_err ( |_| anyhow:: anyhow!( "Invalid CBOR encoded UUID type, invalid bytes size" ) ) ?,
179- ) ;
180- Ok ( uuid)
181- }
182-
183- /// Decode `CBOR` encoded `DocumentRef`.
184- #[ allow( clippy:: indexing_slicing) ]
185- fn decode_cbor_document_ref ( val : & coset:: cbor:: Value ) -> anyhow:: Result < DocumentRef > {
186- if let Ok ( id) = decode_cbor_uuid ( val) {
187- Ok ( DocumentRef :: Latest { id } )
188- } else {
189- let Some ( array) = val. as_array ( ) else {
190- anyhow:: bail!( "Invalid CBOR encoded document `ref` type" ) ;
191- } ;
192- anyhow:: ensure!( array. len( ) == 2 , "Invalid CBOR encoded document `ref` type" ) ;
193- let id = decode_cbor_uuid ( & array[ 0 ] ) ?;
194- let ver = decode_cbor_uuid ( & array[ 1 ] ) ?;
195- Ok ( DocumentRef :: WithVer ( id, ver) )
196- }
197- }
198-
199182/// Find a value for a given key in the protected header.
200- fn cose_protected_header_find ( cose : & coset:: CoseSign , rest_key : & str ) -> Option < ciborium:: Value > {
183+ fn cose_protected_header_find (
184+ cose : & coset:: CoseSign , rest_key : & str ,
185+ ) -> Option < coset:: cbor:: Value > {
201186 cose. protected
202187 . header
203188 . rest
204189 . iter ( )
205190 . find ( |( key, _) | key == & coset:: Label :: Text ( rest_key. to_string ( ) ) )
206191 . map ( |( _, value) | value. clone ( ) )
207192}
208-
209- /// Extract `Metadata` from `coset::CoseSign`.
210- #[ allow( clippy:: too_many_lines) ]
211- fn metadata_from_cose_protected_header ( cose : & coset:: CoseSign ) -> ( Metadata , ContentErrors ) {
212- let expected_header = cose_protected_header ( ) ;
213- let mut errors = Vec :: new ( ) ;
214-
215- if cose. protected . header . content_type != expected_header. content_type {
216- errors. push ( "Invalid COSE document protected header `content-type` field" . to_string ( ) ) ;
217- }
218-
219- if !cose. protected . header . rest . iter ( ) . any ( |( key, value) | {
220- key == & coset:: Label :: Text ( CONTENT_ENCODING_KEY . to_string ( ) )
221- && value == & coset:: cbor:: Value :: Text ( CONTENT_ENCODING_VALUE . to_string ( ) )
222- } ) {
223- errors. push (
224- "Invalid COSE document protected header {CONTENT_ENCODING_KEY} field" . to_string ( ) ,
225- ) ;
226- }
227- let mut metadata = Metadata :: default ( ) ;
228-
229- match cose_protected_header_find ( cose, "type" ) {
230- Some ( doc_type) => {
231- match decode_cbor_uuid ( & doc_type) {
232- Ok ( doc_type_uuid) => {
233- if doc_type_uuid. get_version_num ( ) == 4 {
234- metadata. r#type = doc_type_uuid;
235- } else {
236- errors. push ( format ! (
237- "Document type is not a valid UUIDv4: {doc_type_uuid}"
238- ) ) ;
239- }
240- } ,
241- Err ( e) => {
242- errors. push ( format ! (
243- "Invalid COSE protected header `type` field, err: {e}"
244- ) ) ;
245- } ,
246- }
247- } ,
248- None => errors. push ( "Invalid COSE protected header, missing `type` field" . to_string ( ) ) ,
249- } ;
250-
251- match cose_protected_header_find ( cose, "id" ) {
252- Some ( doc_id) => {
253- match decode_cbor_uuid ( & doc_id) {
254- Ok ( doc_id_uuid) => {
255- if doc_id_uuid. get_version_num ( ) == 7 {
256- metadata. id = doc_id_uuid;
257- } else {
258- errors. push ( format ! ( "Document ID is not a valid UUIDv7: {doc_id_uuid}" ) ) ;
259- }
260- } ,
261- Err ( e) => {
262- errors. push ( format ! (
263- "Invalid COSE protected header `id` field, err: {e}"
264- ) ) ;
265- } ,
266- }
267- } ,
268- None => errors. push ( "Invalid COSE protected header, missing `id` field" . to_string ( ) ) ,
269- } ;
270-
271- match cose_protected_header_find ( cose, "ver" ) {
272- Some ( doc_ver) => {
273- match decode_cbor_uuid ( & doc_ver) {
274- Ok ( doc_ver_uuid) => {
275- let mut is_valid = true ;
276- if doc_ver_uuid. get_version_num ( ) != 7 {
277- errors. push ( format ! (
278- "Document Version is not a valid UUIDv7: {doc_ver_uuid}"
279- ) ) ;
280- is_valid = false ;
281- }
282- if doc_ver_uuid < metadata. id {
283- errors. push ( format ! (
284- "Document Version {doc_ver_uuid} cannot be smaller than Document ID {0}" , metadata. id
285- ) ) ;
286- is_valid = false ;
287- }
288- if is_valid {
289- metadata. ver = doc_ver_uuid;
290- }
291- } ,
292- Err ( e) => {
293- errors. push ( format ! (
294- "Invalid COSE protected header `ver` field, err: {e}"
295- ) ) ;
296- } ,
297- }
298- } ,
299- None => errors. push ( "Invalid COSE protected header, missing `ver` field" . to_string ( ) ) ,
300- }
301-
302- if let Some ( cbor_doc_ref) = cose_protected_header_find ( cose, "ref" ) {
303- match decode_cbor_document_ref ( & cbor_doc_ref) {
304- Ok ( doc_ref) => {
305- metadata. r#ref = Some ( doc_ref) ;
306- } ,
307- Err ( e) => {
308- errors. push ( format ! (
309- "Invalid COSE protected header `ref` field, err: {e}"
310- ) ) ;
311- } ,
312- }
313- }
314-
315- if let Some ( cbor_doc_template) = cose_protected_header_find ( cose, "template" ) {
316- match decode_cbor_document_ref ( & cbor_doc_template) {
317- Ok ( doc_template) => {
318- metadata. template = Some ( doc_template) ;
319- } ,
320- Err ( e) => {
321- errors. push ( format ! (
322- "Invalid COSE protected header `template` field, err: {e}"
323- ) ) ;
324- } ,
325- }
326- }
327-
328- if let Some ( cbor_doc_reply) = cose_protected_header_find ( cose, "reply" ) {
329- match decode_cbor_document_ref ( & cbor_doc_reply) {
330- Ok ( doc_reply) => {
331- metadata. reply = Some ( doc_reply) ;
332- } ,
333- Err ( e) => {
334- errors. push ( format ! (
335- "Invalid COSE protected header `reply` field, err: {e}"
336- ) ) ;
337- } ,
338- }
339- }
340-
341- if let Some ( cbor_doc_section) = cose_protected_header_find ( cose, "section" ) {
342- match cbor_doc_section. into_text ( ) {
343- Ok ( doc_section) => {
344- metadata. section = Some ( doc_section) ;
345- } ,
346- Err ( e) => {
347- errors. push ( format ! (
348- "Invalid COSE protected header `section` field, err: {e:?}"
349- ) ) ;
350- } ,
351- }
352- }
353-
354- ( metadata, ContentErrors ( errors) )
355- }
0 commit comments