Skip to content

Commit 96846f0

Browse files
committed
wip(rust/signed_doc): Impl Display for CatalystSignedDocument, add inspect example
1 parent f06b177 commit 96846f0

File tree

2 files changed

+275
-8
lines changed

2 files changed

+275
-8
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
//! Inspect a Catalyst Signed Document.
2+
use std::{
3+
fs::{
4+
// read_to_string,
5+
File,
6+
},
7+
io::{
8+
Read,
9+
// Write
10+
},
11+
path::PathBuf,
12+
};
13+
14+
use clap::Parser;
15+
use signed_doc::CatalystSignedDocument;
16+
17+
/// Hermes cli commands
18+
#[derive(clap::Parser)]
19+
enum Cli {
20+
/// Inspects COSE document
21+
Inspect {
22+
/// Path to the fully formed (should has at least one signature) COSE document
23+
cose_sign: PathBuf,
24+
/// Path to the json schema (Draft 7) to validate document against it
25+
doc_schema: PathBuf,
26+
},
27+
}
28+
29+
impl Cli {
30+
/// Execute Cli command
31+
fn exec(self) -> anyhow::Result<()> {
32+
match self {
33+
Self::Inspect {
34+
cose_sign,
35+
doc_schema: _,
36+
} => {
37+
//
38+
let mut cose_file = File::open(cose_sign)?;
39+
let mut cose_file_bytes = Vec::new();
40+
cose_file.read_to_end(&mut cose_file_bytes)?;
41+
let cat_signed_doc: CatalystSignedDocument = cose_file_bytes.try_into()?;
42+
println!("{cat_signed_doc}");
43+
Ok(())
44+
},
45+
}
46+
}
47+
}
48+
49+
fn main() {
50+
println!("Catalyst Signed Document");
51+
if let Err(err) = Cli::parse().exec() {
52+
println!("{err}");
53+
}
54+
}

rust/signed_doc/src/lib.rs

Lines changed: 221 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
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.
1633
struct 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)]
4580
pub 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.
6499
impl 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

73130
impl 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

Comments
 (0)