Skip to content

Commit 3cfc329

Browse files
committed
fix(rust/signed_doc): cleanup examples, fix signatures decoding
1 parent ebd29d1 commit 3cfc329

File tree

7 files changed

+68
-299
lines changed

7 files changed

+68
-299
lines changed

rust/signed_doc/examples/cat-signed-doc.rs

Lines changed: 0 additions & 60 deletions
This file was deleted.

rust/signed_doc/examples/mk_signed_doc.rs

Lines changed: 43 additions & 225 deletions
Original file line numberDiff line numberDiff line change
@@ -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};
1212
use clap::Parser;
1313
use 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

1916
fn 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

6058
const CONTENT_ENCODING_KEY: &str = "Content-Encoding";
61-
const CONTENT_ENCODING_VALUE: &str = "br";
6259
const UUID_CBOR_TAG: u64 = 37;
6360

6461
fn 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)]
9769
fn _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+
143131
fn 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-
196167
fn 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

239210
fn 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-
258225
fn 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

Comments
 (0)