11//! Catalyst documents signing crate
2- use std:: { convert:: TryFrom , sync:: Arc } ;
2+ #![ allow( dead_code) ]
3+ use std:: {
4+ convert:: TryFrom ,
5+ fmt:: { Display , Formatter } ,
6+ sync:: Arc ,
7+ } ;
8+
9+ use coset:: CborSerializable ;
310
411/// Keep all the contents private.
512/// Better even to use a structure like this. Wrapping in an Arc means we don't have to
@@ -12,12 +19,26 @@ pub struct CatalystSignedDocument {
1219 content_errors : Vec < String > ,
1320}
1421
22+ impl Display for CatalystSignedDocument {
23+ fn fmt ( & self , f : & mut Formatter < ' _ > ) -> Result < ( ) , std:: fmt:: Error > {
24+ writeln ! ( f, "Metadata: {:?}" , self . inner. metadata) ?;
25+ writeln ! ( f, "JSON Payload: {}" , self . inner. payload) ?;
26+ writeln ! ( f, "Signatures: {:?}" , self . inner. signatures) ?;
27+ write ! ( f, "Content Errors: {:?}" , self . content_errors)
28+ }
29+ }
30+
31+ #[ derive( Default ) ]
1532/// Inner type that holds the Catalyst Signed Document with parsing errors.
1633struct InnerCatalystSignedDocument {
1734 /// Document Metadata
1835 metadata : Metadata ,
19- /// Raw payload
20- _raw_doc : Vec < u8 > ,
36+ /// JSON Payload
37+ payload : serde_json:: Value ,
38+ /// Signatures
39+ signatures : Vec < coset:: CoseSignature > ,
40+ /// Raw COSE Sign bytes
41+ cose_sign : coset:: CoseSign ,
2142}
2243
2344/// Document Metadata.
@@ -39,8 +60,22 @@ pub struct Metadata {
3960 pub section : Option < String > ,
4061}
4162
63+ impl Default for Metadata {
64+ fn default ( ) -> Self {
65+ Self {
66+ r#type : CatalystSignedDocument :: INVALID_UUID ,
67+ id : CatalystSignedDocument :: INVALID_UUID ,
68+ ver : CatalystSignedDocument :: INVALID_UUID ,
69+ r#ref : None ,
70+ template : None ,
71+ reply : None ,
72+ section : None ,
73+ }
74+ }
75+ }
76+
4277/// Reference to a Document.
43- #[ derive( Debug , serde:: Deserialize ) ]
78+ #[ derive( Copy , Clone , Debug , serde:: Deserialize ) ]
4479#[ serde( untagged) ]
4580pub enum DocumentRef {
4681 /// Reference to the latest document
@@ -62,17 +97,39 @@ pub enum DocumentRef {
6297// multiple parameters to actually create the type. This is much more elegant to use this
6398// way, in code.
6499impl TryFrom < Vec < u8 > > for CatalystSignedDocument {
65- type Error = & ' static str ;
100+ type Error = anyhow :: Error ;
66101
67102 #[ allow( clippy:: todo) ]
68- fn try_from ( _value : Vec < u8 > ) -> Result < Self , Self :: Error > {
69- todo ! ( ) ;
103+ fn try_from ( cose_bytes : Vec < u8 > ) -> Result < Self , Self :: Error > {
104+ let cose = coset:: CoseSign :: from_slice ( & cose_bytes)
105+ . map_err ( |e| anyhow:: anyhow!( "Invalid COSE Sign document: {e}" ) ) ?;
106+ let payload = match & cose. payload {
107+ Some ( payload) => {
108+ let mut buf = Vec :: new ( ) ;
109+ let mut bytes = payload. as_slice ( ) ;
110+ brotli:: BrotliDecompress ( & mut bytes, & mut buf) ?;
111+ serde_json:: from_slice ( & buf) ?
112+ } ,
113+ None => {
114+ println ! ( "COSE missing payload field with the JSON content in it" ) ;
115+ serde_json:: Value :: Object ( serde_json:: Map :: new ( ) )
116+ } ,
117+ } ;
118+ let inner = InnerCatalystSignedDocument {
119+ cose_sign : cose,
120+ payload,
121+ ..Default :: default ( )
122+ } ;
123+ Ok ( CatalystSignedDocument {
124+ inner : Arc :: new ( inner) ,
125+ content_errors : Vec :: new ( ) ,
126+ } )
70127 }
71128}
72129
73130impl CatalystSignedDocument {
74131 /// Invalid Doc Type UUID
75- const _INVALID_UUID : uuid:: Uuid = uuid:: Uuid :: from_bytes ( [ 0x00 ; 16 ] ) ;
132+ const INVALID_UUID : uuid:: Uuid = uuid:: Uuid :: from_bytes ( [ 0x00 ; 16 ] ) ;
76133
77134 // A bunch of getters to access the contents, or reason through the document, such as.
78135
@@ -94,3 +151,159 @@ impl CatalystSignedDocument {
94151 self . inner . metadata . id
95152 }
96153}
154+
155+ /// Catalyst Signed Document Content Encoding Key.
156+ const CONTENT_ENCODING_KEY : & str = "content encoding" ;
157+ /// Catalyst Signed Document Content Encoding Value.
158+ const CONTENT_ENCODING_VALUE : & str = "br" ;
159+ /// CBOR tag for UUID content.
160+ const UUID_CBOR_TAG : u64 = 37 ;
161+
162+ /// Generate the COSE protected header used by Catalyst Signed Document.
163+ fn cose_protected_header ( ) -> coset:: Header {
164+ coset:: HeaderBuilder :: new ( )
165+ . algorithm ( coset:: iana:: Algorithm :: EdDSA )
166+ . content_format ( coset:: iana:: CoapContentFormat :: Json )
167+ . text_value (
168+ CONTENT_ENCODING_KEY . to_string ( ) ,
169+ CONTENT_ENCODING_VALUE . to_string ( ) . into ( ) ,
170+ )
171+ . build ( )
172+ }
173+
174+ /// Decode `CBOR` encoded `UUID`.
175+ fn decode_cbor_uuid ( val : & coset:: cbor:: Value ) -> anyhow:: Result < uuid:: Uuid > {
176+ let Some ( ( UUID_CBOR_TAG , coset:: cbor:: Value :: Bytes ( bytes) ) ) = val. as_tag ( ) else {
177+ anyhow:: bail!( "Invalid CBOR encoded UUID type" ) ;
178+ } ;
179+ let uuid = uuid:: Uuid :: from_bytes (
180+ bytes
181+ . clone ( )
182+ . try_into ( )
183+ . map_err ( |_| anyhow:: anyhow!( "Invalid CBOR encoded UUID type, invalid bytes size" ) ) ?,
184+ ) ;
185+ Ok ( uuid)
186+ }
187+
188+ /// Decode `CBOR` encoded `DocumentRef`.
189+ #[ allow( clippy:: indexing_slicing) ]
190+ fn decode_cbor_document_ref ( val : & coset:: cbor:: Value ) -> anyhow:: Result < DocumentRef > {
191+ if let Ok ( id) = decode_cbor_uuid ( val) {
192+ Ok ( DocumentRef :: Latest { id } )
193+ } else {
194+ let Some ( array) = val. as_array ( ) else {
195+ anyhow:: bail!( "Invalid CBOR encoded document `ref` type" ) ;
196+ } ;
197+ anyhow:: ensure!( array. len( ) == 2 , "Invalid CBOR encoded document `ref` type" ) ;
198+ let id = decode_cbor_uuid ( & array[ 0 ] ) ?;
199+ let ver = decode_cbor_uuid ( & array[ 1 ] ) ?;
200+ Ok ( DocumentRef :: WithVer { id, ver } )
201+ }
202+ }
203+
204+ /// Extract `Metadata` from `coset::CoseSign`.
205+ fn validate_cose_protected_header ( cose : & coset:: CoseSign ) -> anyhow:: Result < Metadata > {
206+ let expected_header = cose_protected_header ( ) ;
207+ anyhow:: ensure!(
208+ cose. protected. header. alg == expected_header. alg,
209+ "Invalid COSE document protected header `algorithm` field"
210+ ) ;
211+ anyhow:: ensure!(
212+ cose. protected. header. content_type == expected_header. content_type,
213+ "Invalid COSE document protected header `content-type` field"
214+ ) ;
215+ anyhow:: ensure!(
216+ cose. protected. header. rest. iter( ) . any( |( key, value) | {
217+ key == & coset:: Label :: Text ( CONTENT_ENCODING_KEY . to_string( ) )
218+ && value == & coset:: cbor:: Value :: Text ( CONTENT_ENCODING_VALUE . to_string( ) )
219+ } ) ,
220+ "Invalid COSE document protected header {CONTENT_ENCODING_KEY} field"
221+ ) ;
222+ let mut metadata = Metadata :: default ( ) ;
223+
224+ let Some ( ( _, value) ) = cose
225+ . protected
226+ . header
227+ . rest
228+ . iter ( )
229+ . find ( |( key, _) | key == & coset:: Label :: Text ( "type" . to_string ( ) ) )
230+ else {
231+ anyhow:: bail!( "Invalid COSE protected header, missing `type` field" ) ;
232+ } ;
233+ metadata. r#type = decode_cbor_uuid ( value)
234+ . map_err ( |e| anyhow:: anyhow!( "Invalid COSE protected header `type` field, err: {e}" ) ) ?;
235+
236+ let Some ( ( _, value) ) = cose
237+ . protected
238+ . header
239+ . rest
240+ . iter ( )
241+ . find ( |( key, _) | key == & coset:: Label :: Text ( "id" . to_string ( ) ) )
242+ else {
243+ anyhow:: bail!( "Invalid COSE protected header, missing `id` field" ) ;
244+ } ;
245+ decode_cbor_uuid ( value)
246+ . map_err ( |e| anyhow:: anyhow!( "Invalid COSE protected header `id` field, err: {e}" ) ) ?;
247+
248+ let Some ( ( _, value) ) = cose
249+ . protected
250+ . header
251+ . rest
252+ . iter ( )
253+ . find ( |( key, _) | key == & coset:: Label :: Text ( "ver" . to_string ( ) ) )
254+ else {
255+ anyhow:: bail!( "Invalid COSE protected header, missing `ver` field" ) ;
256+ } ;
257+ decode_cbor_uuid ( value)
258+ . map_err ( |e| anyhow:: anyhow!( "Invalid COSE protected header `ver` field, err: {e}" ) ) ?;
259+
260+ if let Some ( ( _, value) ) = cose
261+ . protected
262+ . header
263+ . rest
264+ . iter ( )
265+ . find ( |( key, _) | key == & coset:: Label :: Text ( "ref" . to_string ( ) ) )
266+ {
267+ decode_cbor_document_ref ( value)
268+ . map_err ( |e| anyhow:: anyhow!( "Invalid COSE protected header `ref` field, err: {e}" ) ) ?;
269+ }
270+
271+ if let Some ( ( _, value) ) = cose
272+ . protected
273+ . header
274+ . rest
275+ . iter ( )
276+ . find ( |( key, _) | key == & coset:: Label :: Text ( "template" . to_string ( ) ) )
277+ {
278+ decode_cbor_document_ref ( value) . map_err ( |e| {
279+ anyhow:: anyhow!( "Invalid COSE protected header `template` field, err: {e}" )
280+ } ) ?;
281+ }
282+
283+ if let Some ( ( _, value) ) = cose
284+ . protected
285+ . header
286+ . rest
287+ . iter ( )
288+ . find ( |( key, _) | key == & coset:: Label :: Text ( "reply" . to_string ( ) ) )
289+ {
290+ decode_cbor_document_ref ( value) . map_err ( |e| {
291+ anyhow:: anyhow!( "Invalid COSE protected header `reply` field, err: {e}" )
292+ } ) ?;
293+ }
294+
295+ if let Some ( ( _, value) ) = cose
296+ . protected
297+ . header
298+ . rest
299+ . iter ( )
300+ . find ( |( key, _) | key == & coset:: Label :: Text ( "section" . to_string ( ) ) )
301+ {
302+ anyhow:: ensure!(
303+ value. is_text( ) ,
304+ "Invalid COSE protected header, missing `section` field"
305+ ) ;
306+ }
307+
308+ Ok ( metadata)
309+ }
0 commit comments