Skip to content

Commit d8c9d03

Browse files
saibatizokustevenjMr-Leshiy
authored
feat(rust/signed-doc): Serialize Signed Documents in CLI tool (#155)
* fix(rust/signed-doc): use tagged cbor for uuid types * feat(rust/signed-doc): add Builder type for signed documents * wip(rust/signed_doc): add CBOR encode for signed doc * feat(rust/signed-doc): example CLI builds signed documents * fix(rust/signed-doc): example cleanup * fix(rust/signed-doc): add algorithm field to metadata * fix(rust/signed-doc): more example cleanup * fix * remove unused jsonschema * fix * refactor * provide consts for key values * refactor, add test * fix * add cbor roundtrip test * fix builder * fix cli * refactor Content type * wip * fix * fix readme.md * fix spelling --------- Co-authored-by: Steven Johnson <[email protected]> Co-authored-by: Mr-Leshiy <[email protected]>
1 parent b9d020f commit d8c9d03

15 files changed

+618
-284
lines changed

rust/signed_doc/Cargo.toml

+1-3
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,10 @@ license.workspace = true
1111
workspace = true
1212

1313
[dependencies]
14-
catalyst-types = { version = "0.0.1", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250114-02" }
14+
catalyst-types = { version = "0.0.1", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250122-00" }
1515
anyhow = "1.0.95"
1616
serde = { version = "1.0.217", features = ["derive"] }
1717
serde_json = "1.0.134"
18-
# TODO: Bump this to the latest version and fix the code
19-
jsonschema = "0.18.3"
2018
coset = "0.3.8"
2119
minicbor = "0.25.1"
2220
brotli = "7.0.0"

rust/signed_doc/README.md

+1-2
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@ Prepare non-signed document,
1717
`meta.json` file should follow the [`meta.schema.json`](./meta.schema.json).
1818

1919
```shell
20-
cargo run -p catalyst-signed-doc --example mk_signed_doc build
21-
signed_doc/doc.json signed_doc/schema.json signed_doc/doc.cose signed_doc/meta.json
20+
cargo run -p catalyst-signed-doc --example mk_signed_doc build signed_doc/doc.json signed_doc/doc.cose signed_doc/meta.json
2221
```
2322

2423
Inspect document

rust/signed_doc/examples/mk_signed_doc.rs

+39-102
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ use std::{
88
path::PathBuf,
99
};
1010

11-
use catalyst_signed_doc::{CatalystSignedDocument, Decode, Decoder, KidUri, Metadata};
11+
use catalyst_signed_doc::{Builder, CatalystSignedDocument, Decode, Decoder, KidUri, Metadata};
1212
use clap::Parser;
13-
use coset::{iana::CoapContentFormat, CborSerializable};
13+
use coset::CborSerializable;
1414
use ed25519_dalek::{ed25519::signature::Signer, pkcs8::DecodePrivateKey};
1515

1616
fn main() {
@@ -19,27 +19,26 @@ fn main() {
1919
}
2020
}
2121

22-
/// Hermes cli commands
22+
/// Catalyst Sign Document CLI Commands
2323
#[derive(clap::Parser)]
24+
#[allow(clippy::large_enum_variant)]
2425
enum Cli {
2526
/// Builds a COSE document without signatures
2627
Build {
2728
/// Path to the document in the json format
2829
doc: PathBuf,
29-
/// Path to the json schema (Draft 7) to validate document against it
30-
schema: PathBuf,
3130
/// Path to the output COSE file to store.
3231
output: PathBuf,
3332
/// Document metadata, must be in JSON format
3433
meta: PathBuf,
3534
},
3635
/// Adds a signature to already formed COSE document
3736
Sign {
38-
/// Path to the secret key in PEM format
39-
sk: PathBuf,
4037
/// Path to the formed (could be empty, without any signatures) COSE document
4138
/// This exact file would be modified and new signature would be added
4239
doc: PathBuf,
40+
/// Path to the secret key in PEM format
41+
sk: PathBuf,
4342
/// Signer kid
4443
kid: KidUri,
4544
},
@@ -55,34 +54,28 @@ enum Cli {
5554
},
5655
}
5756

58-
const CONTENT_ENCODING_KEY: &str = "Content-Encoding";
59-
const UUID_CBOR_TAG: u64 = 37;
60-
61-
fn encode_cbor_uuid(uuid: &uuid::Uuid) -> coset::cbor::Value {
62-
coset::cbor::Value::Tag(
63-
UUID_CBOR_TAG,
64-
coset::cbor::Value::Bytes(uuid.as_bytes().to_vec()).into(),
65-
)
66-
}
67-
6857
impl Cli {
6958
fn exec(self) -> anyhow::Result<()> {
7059
match self {
71-
Self::Build {
72-
doc,
73-
schema,
74-
output,
75-
meta,
76-
} => {
77-
let doc_schema = load_schema_from_file(&schema)?;
78-
let json_doc = load_json_from_file(&doc)?;
79-
let json_meta = load_json_from_file(&meta)
60+
Self::Build { doc, output, meta } => {
61+
// Load Metadata from JSON file
62+
let metadata: Metadata = load_json_from_file(&meta)
8063
.map_err(|e| anyhow::anyhow!("Failed to load metadata from file: {e}"))?;
81-
println!("{json_meta}");
82-
validate_json(&json_doc, &doc_schema)?;
83-
let compressed_doc = brotli_compress_json(&json_doc)?;
84-
let empty_cose_sign = build_empty_cose_doc(compressed_doc, &json_meta);
85-
store_cose_file(empty_cose_sign, &output)?;
64+
println!("{metadata}");
65+
// Load Document from JSON file
66+
let json_doc: serde_json::Value = load_json_from_file(&doc)?;
67+
// Possibly encode if Metadata has an encoding set.
68+
let payload = serde_json::to_vec(&json_doc)?;
69+
// Start with no signatures.
70+
let signed_doc = Builder::new()
71+
.with_content(payload)
72+
.with_metadata(metadata)
73+
.build()?;
74+
let mut bytes: Vec<u8> = Vec::new();
75+
minicbor::encode(signed_doc, &mut bytes)
76+
.map_err(|e| anyhow::anyhow!("Failed to encode document: {e}"))?;
77+
78+
write_bytes_to_file(&bytes, &output)?;
8679
},
8780
Self::Sign { sk, doc, kid } => {
8881
let sk = load_secret_key_from_file(&sk)
@@ -116,99 +109,43 @@ fn decode_signed_doc(cose_bytes: &[u8]) {
116109
);
117110
match CatalystSignedDocument::decode(&mut Decoder::new(cose_bytes), &mut ()) {
118111
Ok(cat_signed_doc) => {
119-
println!("This is a valid Catalyst Signed Document.");
112+
println!("This is a valid Catalyst Document.");
120113
println!("{cat_signed_doc}");
121114
},
122-
Err(e) => eprintln!("Invalid Catalyst Signed Document, err: {e}"),
115+
Err(e) => eprintln!("Invalid Catalyst Document, err: {e}"),
123116
}
124117
}
125118

126-
fn load_schema_from_file(schema_path: &PathBuf) -> anyhow::Result<jsonschema::JSONSchema> {
127-
let schema_file = File::open(schema_path)?;
128-
let schema_json = serde_json::from_reader(schema_file)?;
129-
let schema = jsonschema::JSONSchema::options()
130-
.with_draft(jsonschema::Draft::Draft7)
131-
.compile(&schema_json)
132-
.map_err(|e| anyhow::anyhow!("{e}"))?;
133-
Ok(schema)
134-
}
135-
136119
fn load_json_from_file<T>(path: &PathBuf) -> anyhow::Result<T>
137120
where T: for<'de> serde::Deserialize<'de> {
138121
let file = File::open(path)?;
139122
let json = serde_json::from_reader(file)?;
140123
Ok(json)
141124
}
142125

143-
fn validate_json(doc: &serde_json::Value, schema: &jsonschema::JSONSchema) -> anyhow::Result<()> {
144-
schema.validate(doc).map_err(|err| {
145-
let mut validation_error = String::new();
146-
for e in err {
147-
validation_error.push_str(&format!("\n - {e}"));
148-
}
149-
anyhow::anyhow!("{validation_error}")
150-
})?;
151-
Ok(())
152-
}
153-
154-
fn brotli_compress_json(doc: &serde_json::Value) -> anyhow::Result<Vec<u8>> {
155-
let brotli_params = brotli::enc::BrotliEncoderParams::default();
156-
let doc_bytes = serde_json::to_vec(&doc)?;
157-
let mut buf = Vec::new();
158-
brotli::BrotliCompress(&mut doc_bytes.as_slice(), &mut buf, &brotli_params)?;
159-
Ok(buf)
126+
fn load_cose_from_file(cose_path: &PathBuf) -> anyhow::Result<coset::CoseSign> {
127+
let cose_file_bytes = read_bytes_from_file(cose_path)?;
128+
let cose = coset::CoseSign::from_slice(&cose_file_bytes).map_err(|e| anyhow::anyhow!("{e}"))?;
129+
Ok(cose)
160130
}
161131

162-
fn build_empty_cose_doc(doc_bytes: Vec<u8>, meta: &Metadata) -> coset::CoseSign {
163-
let mut builder =
164-
coset::HeaderBuilder::new().content_format(CoapContentFormat::from(meta.content_type()));
165-
166-
if let Some(content_encoding) = meta.content_encoding() {
167-
builder = builder.text_value(
168-
CONTENT_ENCODING_KEY.to_string(),
169-
format!("{content_encoding}").into(),
170-
);
171-
}
172-
let mut protected_header = builder.build();
173-
174-
protected_header.rest.push((
175-
coset::Label::Text("type".to_string()),
176-
encode_cbor_uuid(&meta.doc_type()),
177-
));
178-
protected_header.rest.push((
179-
coset::Label::Text("id".to_string()),
180-
encode_cbor_uuid(&meta.doc_id()),
181-
));
182-
protected_header.rest.push((
183-
coset::Label::Text("ver".to_string()),
184-
encode_cbor_uuid(&meta.doc_ver()),
185-
));
186-
let meta_rest = meta.extra().header_rest().unwrap_or_default();
187-
188-
if !meta_rest.is_empty() {
189-
protected_header.rest.extend(meta_rest);
190-
}
191-
coset::CoseSignBuilder::new()
192-
.protected(protected_header)
193-
.payload(doc_bytes)
194-
.build()
132+
fn read_bytes_from_file(path: &PathBuf) -> anyhow::Result<Vec<u8>> {
133+
let mut file_bytes = Vec::new();
134+
File::open(path)?.read_to_end(&mut file_bytes)?;
135+
Ok(file_bytes)
195136
}
196137

197-
fn load_cose_from_file(cose_path: &PathBuf) -> anyhow::Result<coset::CoseSign> {
198-
let mut cose_file = File::open(cose_path)?;
199-
let mut cose_file_bytes = Vec::new();
200-
cose_file.read_to_end(&mut cose_file_bytes)?;
201-
let cose = coset::CoseSign::from_slice(&cose_file_bytes).map_err(|e| anyhow::anyhow!("{e}"))?;
202-
Ok(cose)
138+
fn write_bytes_to_file(bytes: &[u8], output: &PathBuf) -> anyhow::Result<()> {
139+
File::create(output)?
140+
.write_all(bytes)
141+
.map_err(|e| anyhow::anyhow!("Failed to write to file {output:?}: {e}"))
203142
}
204143

205144
fn store_cose_file(cose: coset::CoseSign, output: &PathBuf) -> anyhow::Result<()> {
206-
let mut cose_file = File::create(output)?;
207145
let cose_bytes = cose
208146
.to_vec()
209147
.map_err(|e| anyhow::anyhow!("Failed to Store COSE SIGN: {e}"))?;
210-
cose_file.write_all(&cose_bytes)?;
211-
Ok(())
148+
write_bytes_to_file(&cose_bytes, output)
212149
}
213150

214151
fn load_secret_key_from_file(sk_path: &PathBuf) -> anyhow::Result<ed25519_dalek::SigningKey> {

rust/signed_doc/src/builder.rs

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
//! Catalyst Signed Document Builder.
2+
use crate::{CatalystSignedDocument, Content, InnerCatalystSignedDocument, Metadata, Signatures};
3+
4+
/// Catalyst Signed Document Builder.
5+
#[derive(Debug, Default)]
6+
pub struct Builder {
7+
/// Document Metadata
8+
metadata: Option<Metadata>,
9+
/// Document Content
10+
content: Option<Vec<u8>>,
11+
/// Signatures
12+
signatures: Signatures,
13+
}
14+
15+
impl Builder {
16+
/// Start building a signed document
17+
#[must_use]
18+
pub fn new() -> Self {
19+
Self::default()
20+
}
21+
22+
/// Set document metadata
23+
#[must_use]
24+
pub fn with_metadata(mut self, metadata: Metadata) -> Self {
25+
self.metadata = Some(metadata);
26+
self
27+
}
28+
29+
/// Set document content
30+
#[must_use]
31+
pub fn with_content(mut self, content: Vec<u8>) -> Self {
32+
self.content = Some(content);
33+
self
34+
}
35+
36+
/// Build a signed document
37+
///
38+
/// ## Errors
39+
///
40+
/// Fails if any of the fields are missing.
41+
pub fn build(self) -> anyhow::Result<CatalystSignedDocument> {
42+
let Some(metadata) = self.metadata else {
43+
anyhow::bail!("Failed to build Catalyst Signed Document, missing metadata");
44+
};
45+
let Some(content) = self.content else {
46+
anyhow::bail!("Failed to build Catalyst Signed Document, missing document's content");
47+
};
48+
let signatures = self.signatures;
49+
50+
let content = Content::from_decoded(
51+
content,
52+
metadata.content_type(),
53+
metadata.content_encoding(),
54+
)?;
55+
56+
Ok(InnerCatalystSignedDocument {
57+
metadata,
58+
content,
59+
signatures,
60+
}
61+
.into())
62+
}
63+
}

0 commit comments

Comments
 (0)