@@ -8,13 +8,10 @@ use std::{
88 path:: PathBuf ,
99} ;
1010
11- use catalyst_signed_doc:: { DocumentRef , KidUri , Metadata } ;
11+ use catalyst_signed_doc:: { CatalystSignedDocument , Decode , Decoder , DocumentRef , KidUri , Metadata } ;
1212use clap:: Parser ;
1313use coset:: { iana:: CoapContentFormat , CborSerializable } ;
14- use ed25519_dalek:: {
15- ed25519:: signature:: Signer ,
16- pkcs8:: { DecodePrivateKey , DecodePublicKey } ,
17- } ;
14+ use ed25519_dalek:: { ed25519:: signature:: Signer , pkcs8:: DecodePrivateKey } ;
1815
1916fn main ( ) {
2017 if let Err ( err) = Cli :: parse ( ) . exec ( ) {
@@ -44,21 +41,21 @@ enum Cli {
4441 /// This exact file would be modified and new signature would be added
4542 doc : PathBuf ,
4643 /// Signer kid
47- kid : String ,
44+ kid : KidUri ,
4845 } ,
49- /// Verifies COSE document
50- Verify {
51- /// Path to the public key in PEM format
52- pk : PathBuf ,
46+ /// Inspects Catalyst Signed Document
47+ Inspect {
5348 /// Path to the fully formed (should has at least one signature) COSE document
54- doc : PathBuf ,
55- /// Path to the json schema (Draft 7) to validate document against it
56- schema : PathBuf ,
49+ path : PathBuf ,
50+ } ,
51+ /// Inspects Catalyst Signed Document from hex-encoded bytes
52+ InspectBytes {
53+ /// Hex-formatted COSE SIGN Bytes
54+ cose_sign_hex : String ,
5755 } ,
5856}
5957
6058const CONTENT_ENCODING_KEY : & str = "Content-Encoding" ;
61- const CONTENT_ENCODING_VALUE : & str = "br" ;
6259const UUID_CBOR_TAG : u64 = 37 ;
6360
6461fn encode_cbor_uuid ( uuid : & uuid:: Uuid ) -> coset:: cbor:: Value {
@@ -68,31 +65,6 @@ fn encode_cbor_uuid(uuid: &uuid::Uuid) -> coset::cbor::Value {
6865 )
6966}
7067
71- fn _decode_cbor_uuid ( val : & coset:: cbor:: Value ) -> anyhow:: Result < uuid:: Uuid > {
72- let Some ( ( UUID_CBOR_TAG , coset:: cbor:: Value :: Bytes ( bytes) ) ) = val. as_tag ( ) else {
73- anyhow:: bail!( "Invalid CBOR encoded UUID type" ) ;
74- } ;
75- let uuid = uuid:: Uuid :: from_bytes (
76- bytes
77- . clone ( )
78- . try_into ( )
79- . map_err ( |_| anyhow:: anyhow!( "Invalid CBOR encoded UUID type, invalid bytes size" ) ) ?,
80- ) ;
81- Ok ( uuid)
82- }
83-
84- fn _encode_cbor_document_ref ( doc_ref : & DocumentRef ) -> coset:: cbor:: Value {
85- match doc_ref {
86- DocumentRef :: Latest { id } => encode_cbor_uuid ( & id. uuid ( ) ) ,
87- DocumentRef :: WithVer { id, ver } => {
88- coset:: cbor:: Value :: Array ( vec ! [
89- encode_cbor_uuid( & id. uuid( ) ) ,
90- encode_cbor_uuid( & ver. uuid( ) ) ,
91- ] )
92- } ,
93- }
94- }
95-
9668#[ allow( clippy:: indexing_slicing) ]
9769fn _decode_cbor_document_ref ( val : & coset:: cbor:: Value ) -> anyhow:: Result < DocumentRef > {
9870 DocumentRef :: try_from ( val)
@@ -118,28 +90,44 @@ impl Cli {
11890 store_cose_file ( empty_cose_sign, & output) ?;
11991 } ,
12092 Self :: Sign { sk, doc, kid } => {
121- let sk = load_secret_key_from_file ( & sk) ?;
122- let mut cose = load_cose_from_file ( & doc) ?;
123- add_signature_to_cose ( & mut cose, & sk, kid) ;
93+ let sk = load_secret_key_from_file ( & sk)
94+ . map_err ( |e| anyhow:: anyhow!( "Failed to load SK FILE: {e}" ) ) ?;
95+ let mut cose = load_cose_from_file ( & doc)
96+ . map_err ( |e| anyhow:: anyhow!( "Failed to load COSE FROM FILE: {e}" ) ) ?;
97+ add_signature_to_cose ( & mut cose, & sk, kid. to_string ( ) ) ;
12498 store_cose_file ( cose, & doc) ?;
12599 } ,
126- Self :: Verify { pk, doc, schema } => {
127- let pk = load_public_key_from_file ( & pk)
128- . map_err ( |e| anyhow:: anyhow!( "Failed to load public key from file: {e}" ) ) ?;
129- let schema = load_schema_from_file ( & schema) . map_err ( |e| {
130- anyhow:: anyhow!( "Failed to load document schema from file: {e}" )
131- } ) ?;
132- let cose = load_cose_from_file ( & doc)
133- . map_err ( |e| anyhow:: anyhow!( "Failed to load COSE SIGN from file: {e}" ) ) ?;
134- validate_cose ( & cose, & pk, & schema) ?;
135- println ! ( "Document is valid." ) ;
100+ Self :: Inspect { path } => {
101+ let mut cose_file = File :: open ( path) ?;
102+ let mut cose_bytes = Vec :: new ( ) ;
103+ cose_file. read_to_end ( & mut cose_bytes) ?;
104+ decode_signed_doc ( & cose_bytes) ;
105+ } ,
106+ Self :: InspectBytes { cose_sign_hex } => {
107+ let cose_bytes = hex:: decode ( & cose_sign_hex) ?;
108+ decode_signed_doc ( & cose_bytes) ;
136109 } ,
137110 }
138111 println ! ( "Done" ) ;
139112 Ok ( ( ) )
140113 }
141114}
142115
116+ fn decode_signed_doc ( cose_bytes : & [ u8 ] ) {
117+ println ! (
118+ "Decoding {} bytes: {}" ,
119+ cose_bytes. len( ) ,
120+ hex:: encode( cose_bytes)
121+ ) ;
122+ match CatalystSignedDocument :: decode ( & mut Decoder :: new ( cose_bytes) , & mut ( ) ) {
123+ Ok ( cat_signed_doc) => {
124+ println ! ( "This is a valid Catalyst Signed Document." ) ;
125+ println ! ( "{cat_signed_doc}" ) ;
126+ } ,
127+ Err ( e) => eprintln ! ( "Invalid Cataylyst Signed Document, err: {e}" ) ,
128+ }
129+ }
130+
143131fn load_schema_from_file ( schema_path : & PathBuf ) -> anyhow:: Result < jsonschema:: JSONSchema > {
144132 let schema_file = File :: open ( schema_path) ?;
145133 let schema_json = serde_json:: from_reader ( schema_file) ?;
@@ -176,23 +164,6 @@ fn brotli_compress_json(doc: &serde_json::Value) -> anyhow::Result<Vec<u8>> {
176164 Ok ( buf)
177165}
178166
179- fn brotli_decompress_json ( mut doc_bytes : & [ u8 ] ) -> anyhow:: Result < serde_json:: Value > {
180- let mut buf = Vec :: new ( ) ;
181- brotli:: BrotliDecompress ( & mut doc_bytes, & mut buf) ?;
182- let json_doc = serde_json:: from_slice ( & buf) ?;
183- Ok ( json_doc)
184- }
185-
186- fn cose_protected_header ( ) -> coset:: Header {
187- coset:: HeaderBuilder :: new ( )
188- . content_format ( CoapContentFormat :: Json )
189- . text_value (
190- CONTENT_ENCODING_KEY . to_string ( ) ,
191- CONTENT_ENCODING_VALUE . to_string ( ) . into ( ) ,
192- )
193- . build ( )
194- }
195-
196167fn build_empty_cose_doc ( doc_bytes : Vec < u8 > , meta : & Metadata ) -> coset:: CoseSign {
197168 let mut builder =
198169 coset:: HeaderBuilder :: new ( ) . content_format ( CoapContentFormat :: from ( meta. content_type ( ) ) ) ;
@@ -238,7 +209,9 @@ fn load_cose_from_file(cose_path: &PathBuf) -> anyhow::Result<coset::CoseSign> {
238209
239210fn store_cose_file ( cose : coset:: CoseSign , output : & PathBuf ) -> anyhow:: Result < ( ) > {
240211 let mut cose_file = File :: create ( output) ?;
241- let cose_bytes = cose. to_vec ( ) . map_err ( |e| anyhow:: anyhow!( "{e}" ) ) ?;
212+ let cose_bytes = cose
213+ . to_vec ( )
214+ . map_err ( |e| anyhow:: anyhow!( "Failed to Store COSE SIGN: {e}" ) ) ?;
242215 cose_file. write_all ( & cose_bytes) ?;
243216 Ok ( ( ) )
244217}
@@ -249,12 +222,6 @@ fn load_secret_key_from_file(sk_path: &PathBuf) -> anyhow::Result<ed25519_dalek:
249222 Ok ( sk)
250223}
251224
252- fn load_public_key_from_file ( pk_path : & PathBuf ) -> anyhow:: Result < ed25519_dalek:: VerifyingKey > {
253- let pk_str = read_to_string ( pk_path) ?;
254- let pk = ed25519_dalek:: VerifyingKey :: from_public_key_pem ( & pk_str) ?;
255- Ok ( pk)
256- }
257-
258225fn add_signature_to_cose ( cose : & mut coset:: CoseSign , sk : & ed25519_dalek:: SigningKey , kid : String ) {
259226 let protected_header = coset:: HeaderBuilder :: new ( )
260227 . key_id ( kid. into_bytes ( ) )
@@ -266,152 +233,3 @@ fn add_signature_to_cose(cose: &mut coset::CoseSign, sk: &ed25519_dalek::Signing
266233 signature. signature = sk. sign ( & data_to_sign) . to_vec ( ) ;
267234 cose. signatures . push ( signature) ;
268235}
269-
270- fn validate_cose (
271- cose : & coset:: CoseSign , pk : & ed25519_dalek:: VerifyingKey , schema : & jsonschema:: JSONSchema ,
272- ) -> anyhow:: Result < ( ) > {
273- validate_cose_protected_header ( cose) ?;
274-
275- let Some ( payload) = & cose. payload else {
276- anyhow:: bail!( "COSE missing payload field with the JSON content in it" ) ;
277- } ;
278- let json_doc = brotli_decompress_json ( payload. as_slice ( ) ) ?;
279- validate_json ( & json_doc, schema) ?;
280-
281- for sign in & cose. signatures {
282- let key_id = & sign. protected . header . key_id ;
283- anyhow:: ensure!(
284- !key_id. is_empty( ) ,
285- "COSE missing signature protected header `kid` field "
286- ) ;
287-
288- let kid = KidUri :: try_from ( key_id. as_ref ( ) ) ?;
289- println ! ( "Signature Key ID: {kid}" ) ;
290- let data_to_sign = cose. tbs_data ( & [ ] , sign) ;
291- let signature_bytes = sign. signature . as_slice ( ) . try_into ( ) . map_err ( |_| {
292- anyhow:: anyhow!(
293- "Invalid signature bytes size: expected {}, provided {}." ,
294- ed25519_dalek:: Signature :: BYTE_SIZE ,
295- sign. signature. len( )
296- )
297- } ) ?;
298- println ! (
299- "Verifying Key Len({}): 0x{}" ,
300- pk. as_bytes( ) . len( ) ,
301- hex:: encode( pk. as_bytes( ) )
302- ) ;
303- let signature = ed25519_dalek:: Signature :: from_bytes ( signature_bytes) ;
304- pk. verify_strict ( & data_to_sign, & signature) ?;
305- }
306-
307- Ok ( ( ) )
308- }
309-
310- fn validate_cose_protected_header ( cose : & coset:: CoseSign ) -> anyhow:: Result < ( ) > {
311- let expected_header = cose_protected_header ( ) ;
312- anyhow:: ensure!(
313- cose. protected. header. alg == expected_header. alg,
314- "Invalid COSE document protected header `algorithm` field"
315- ) ;
316- anyhow:: ensure!(
317- cose. protected. header. content_type == expected_header. content_type,
318- "Invalid COSE document protected header `content-type` field"
319- ) ;
320- println ! ( "HEADER REST: \n {:?}" , cose. protected. header. rest) ;
321- anyhow:: ensure!(
322- cose. protected. header. rest. iter( ) . any( |( key, value) | {
323- key == & coset:: Label :: Text ( CONTENT_ENCODING_KEY . to_string( ) )
324- && value == & coset:: cbor:: Value :: Text ( CONTENT_ENCODING_VALUE . to_string( ) )
325- } ) ,
326- "Invalid COSE document protected header"
327- ) ;
328-
329- // let Some((_, value)) = cose
330- // .protected
331- // .header
332- // .rest
333- // .iter()
334- // .find(|(key, _)| key == &coset::Label::Text("type".to_string()))
335- // else {
336- // anyhow::bail!("Invalid COSE protected header, missing `type` field");
337- // };
338- // decode_cbor_uuid(value)
339- // .map_err(|e| anyhow::anyhow!("Invalid COSE protected header `type` field, err:
340- // {e}"))?;
341-
342- // let Some((_, value)) = cose
343- // .protected
344- // .header
345- // .rest
346- // .iter()
347- // .find(|(key, _)| key == &coset::Label::Text("id".to_string()))
348- // else {
349- // anyhow::bail!("Invalid COSE protected header, missing `id` field");
350- // };
351- // decode_cbor_uuid(value)
352- // .map_err(|e| anyhow::anyhow!("Invalid COSE protected header `id` field, err:
353- // {e}"))?;
354-
355- // let Some((_, value)) = cose
356- // .protected
357- // .header
358- // .rest
359- // .iter()
360- // .find(|(key, _)| key == &coset::Label::Text("ver".to_string()))
361- // else {
362- // anyhow::bail!("Invalid COSE protected header, missing `ver` field");
363- // };
364- // decode_cbor_uuid(value)
365- // .map_err(|e| anyhow::anyhow!("Invalid COSE protected header `ver` field, err:
366- // {e}"))?;
367-
368- // if let Some((_, value)) = cose
369- // .protected
370- // .header
371- // .rest
372- // .iter()
373- // .find(|(key, _)| key == &coset::Label::Text("ref".to_string()))
374- // {
375- // decode_cbor_document_ref(value)
376- // .map_err(|e| anyhow::anyhow!("Invalid COSE protected header `ref` field, err:
377- // {e}"))?; }
378-
379- // if let Some((_, value)) = cose
380- // .protected
381- // .header
382- // .rest
383- // .iter()
384- // .find(|(key, _)| key == &coset::Label::Text("template".to_string()))
385- // {
386- // decode_cbor_document_ref(value).map_err(|e| {
387- // anyhow::anyhow!("Invalid COSE protected header `template` field, err: {e}")
388- // })?;
389- // }
390-
391- // if let Some((_, value)) = cose
392- // .protected
393- // .header
394- // .rest
395- // .iter()
396- // .find(|(key, _)| key == &coset::Label::Text("reply".to_string()))
397- // {
398- // decode_cbor_document_ref(value).map_err(|e| {
399- // anyhow::anyhow!("Invalid COSE protected header `reply` field, err: {e}")
400- // })?;
401- // }
402-
403- // if let Some((_, value)) = cose
404- // .protected
405- // .header
406- // .rest
407- // .iter()
408- // .find(|(key, _)| key == &coset::Label::Text("section".to_string()))
409- // {
410- // anyhow::ensure!(
411- // value.is_text(),
412- // "Invalid COSE protected header, missing `section` field"
413- // );
414- // }
415-
416- Ok ( ( ) )
417- }
0 commit comments