Skip to content

Commit 14d500a

Browse files
committed
feat(rust/signed-doc): example CLI builds signed documents
1 parent b22dc8f commit 14d500a

File tree

8 files changed

+150
-62
lines changed

8 files changed

+150
-62
lines changed

rust/signed_doc/examples/mk_signed_doc.rs

Lines changed: 53 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ use std::{
88
path::PathBuf,
99
};
1010

11-
use catalyst_signed_doc::{CatalystSignedDocument, Decode, Decoder, KidUri, Metadata};
11+
use catalyst_signed_doc::{
12+
Builder, CatalystSignedDocument, Content, Decode, Decoder, KidUri, Metadata, Signatures,
13+
};
1214
use clap::Parser;
1315
use coset::{CborSerializable, Header};
1416
use ed25519_dalek::{ed25519::signature::Signer, pkcs8::DecodePrivateKey};
@@ -60,19 +62,39 @@ impl Cli {
6062
match self {
6163
Self::Build {
6264
doc,
63-
schema,
65+
schema: _,
6466
output,
6567
meta,
6668
} => {
67-
let doc_schema = load_schema_from_file(&schema)?;
68-
let json_doc = load_json_from_file(&doc)?;
69-
let json_meta = &load_json_from_file(&meta)
69+
// Load Metadata from JSON file
70+
let metadata: Metadata = load_json_from_file(&meta)
7071
.map_err(|e| anyhow::anyhow!("Failed to load metadata from file: {e}"))?;
71-
println!("{json_meta}");
72-
validate_json(&json_doc, &doc_schema)?;
73-
let compressed_doc = brotli_compress_json(&json_doc)?;
74-
let empty_cose_sign = build_empty_cose_doc(compressed_doc, json_meta)?;
75-
store_cose_file(empty_cose_sign, &output)?;
72+
println!("{metadata}");
73+
// Load Document from JSON file
74+
let json_doc: serde_json::Value = load_json_from_file(&doc)?;
75+
// Possibly encode if Metadata has an encoding set.
76+
let payload_bytes = serde_json::to_vec(&json_doc)?;
77+
let payload = match metadata.content_encoding() {
78+
Some(encoding) => encoding.encode(&payload_bytes)?,
79+
None => payload_bytes,
80+
};
81+
let content = Content::new(
82+
payload,
83+
metadata.content_type(),
84+
metadata.content_encoding(),
85+
)?;
86+
// Start with no signatures.
87+
let signatures = Signatures::try_from(&Vec::new())?;
88+
let signed_doc = Builder::new()
89+
.content(content)
90+
.metadata(metadata)
91+
.signatures(signatures)
92+
.build()?;
93+
let mut bytes: Vec<u8> = Vec::new();
94+
minicbor::encode(signed_doc, &mut bytes)
95+
.map_err(|e| anyhow::anyhow!("Failed to encode document: {e}"))?;
96+
97+
write_bytes_to_file(&bytes, &output)?;
7698
},
7799
Self::Sign { sk, doc, kid } => {
78100
let sk = load_secret_key_from_file(&sk)
@@ -106,14 +128,14 @@ fn decode_signed_doc(cose_bytes: &[u8]) {
106128
);
107129
match CatalystSignedDocument::decode(&mut Decoder::new(cose_bytes), &mut ()) {
108130
Ok(cat_signed_doc) => {
109-
println!("This is a valid Catalyst Signed Document.");
131+
println!("This is a valid Catalyst Document.");
110132
println!("{cat_signed_doc}");
111133
},
112-
Err(e) => eprintln!("Invalid Catalyst Signed Document, err: {e}"),
134+
Err(e) => eprintln!("Invalid Catalyst Document, err: {e}"),
113135
}
114136
}
115137

116-
fn load_schema_from_file(schema_path: &PathBuf) -> anyhow::Result<jsonschema::JSONSchema> {
138+
fn _load_schema_from_file(schema_path: &PathBuf) -> anyhow::Result<jsonschema::JSONSchema> {
117139
let schema_file = File::open(schema_path)?;
118140
let schema_json = serde_json::from_reader(schema_file)?;
119141
let schema = jsonschema::JSONSchema::options()
@@ -130,7 +152,7 @@ where T: for<'de> serde::Deserialize<'de> {
130152
Ok(json)
131153
}
132154

133-
fn validate_json(doc: &serde_json::Value, schema: &jsonschema::JSONSchema) -> anyhow::Result<()> {
155+
fn _validate_json(doc: &serde_json::Value, schema: &jsonschema::JSONSchema) -> anyhow::Result<()> {
134156
schema.validate(doc).map_err(|err| {
135157
let mut validation_error = String::new();
136158
for e in err {
@@ -141,15 +163,15 @@ fn validate_json(doc: &serde_json::Value, schema: &jsonschema::JSONSchema) -> an
141163
Ok(())
142164
}
143165

144-
fn brotli_compress_json(doc: &serde_json::Value) -> anyhow::Result<Vec<u8>> {
166+
fn _brotli_compress_json(doc: &serde_json::Value) -> anyhow::Result<Vec<u8>> {
145167
let brotli_params = brotli::enc::BrotliEncoderParams::default();
146-
let doc_bytes = serde_json::to_vec(&doc)?;
168+
let doc_bytes = serde_json::to_vec(doc)?;
147169
let mut buf = Vec::new();
148170
brotli::BrotliCompress(&mut doc_bytes.as_slice(), &mut buf, &brotli_params)?;
149171
Ok(buf)
150172
}
151173

152-
fn build_empty_cose_doc(doc_bytes: Vec<u8>, meta: &Metadata) -> anyhow::Result<coset::CoseSign> {
174+
fn _build_empty_cose_doc(doc_bytes: Vec<u8>, meta: &Metadata) -> anyhow::Result<coset::CoseSign> {
153175
let protected_header = Header::try_from(meta)?;
154176
Ok(coset::CoseSignBuilder::new()
155177
.protected(protected_header)
@@ -158,20 +180,28 @@ fn build_empty_cose_doc(doc_bytes: Vec<u8>, meta: &Metadata) -> anyhow::Result<c
158180
}
159181

160182
fn load_cose_from_file(cose_path: &PathBuf) -> anyhow::Result<coset::CoseSign> {
161-
let mut cose_file = File::open(cose_path)?;
162-
let mut cose_file_bytes = Vec::new();
163-
cose_file.read_to_end(&mut cose_file_bytes)?;
183+
let cose_file_bytes = read_bytes_from_file(cose_path)?;
164184
let cose = coset::CoseSign::from_slice(&cose_file_bytes).map_err(|e| anyhow::anyhow!("{e}"))?;
165185
Ok(cose)
166186
}
167187

188+
fn read_bytes_from_file(path: &PathBuf) -> anyhow::Result<Vec<u8>> {
189+
let mut file_bytes = Vec::new();
190+
File::open(path)?.read_to_end(&mut file_bytes)?;
191+
Ok(file_bytes)
192+
}
193+
194+
fn write_bytes_to_file(bytes: &[u8], output: &PathBuf) -> anyhow::Result<()> {
195+
File::create(output)?
196+
.write_all(bytes)
197+
.map_err(|e| anyhow::anyhow!("Failed to write to file {output:?}: {e}"))
198+
}
199+
168200
fn store_cose_file(cose: coset::CoseSign, output: &PathBuf) -> anyhow::Result<()> {
169-
let mut cose_file = File::create(output)?;
170201
let cose_bytes = cose
171202
.to_vec()
172203
.map_err(|e| anyhow::anyhow!("Failed to Store COSE SIGN: {e}"))?;
173-
cose_file.write_all(&cose_bytes)?;
174-
Ok(())
204+
write_bytes_to_file(&cose_bytes, output)
175205
}
176206

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

rust/signed_doc/src/builder.rs

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ use crate::{CatalystSignedDocument, Content, InnerCatalystSignedDocument, Metada
33

44
/// Catalyst Signed Document Builder.
55
#[derive(Debug, Default)]
6-
#[allow(dead_code)]
76
pub struct Builder {
87
/// Document Metadata
98
metadata: Option<Metadata>,
@@ -45,18 +44,16 @@ impl Builder {
4544
///
4645
/// ## Errors
4746
///
48-
/// Returns
47+
/// Fails if any of the fields are missing.
4948
pub fn build(self) -> anyhow::Result<CatalystSignedDocument> {
5049
match (self.metadata, self.content, self.signatures) {
5150
(Some(metadata), Some(content), Some(signatures)) => {
52-
Ok(CatalystSignedDocument {
53-
inner: InnerCatalystSignedDocument {
54-
metadata,
55-
content,
56-
signatures,
57-
}
58-
.into(),
59-
})
51+
Ok(InnerCatalystSignedDocument {
52+
metadata,
53+
content,
54+
signatures,
55+
}
56+
.into())
6057
},
6158
_ => Err(anyhow::anyhow!("Failed to build Catalyst Signed Document")),
6259
}

rust/signed_doc/src/content.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ impl Content {
1010
/// Creates a new `Content` value,
1111
/// verifies a Document's content, that it is correctly encoded and it corresponds and
1212
/// parsed to the specified type
13+
///
14+
/// # Errors
15+
/// Returns an error if content is not correctly encoded
1316
pub fn new(
1417
mut content: Vec<u8>, content_type: ContentType, encoding: Option<ContentEncoding>,
1518
) -> anyhow::Result<Self> {
@@ -35,7 +38,20 @@ impl Content {
3538
}
3639

3740
/// Return content bytes
41+
#[must_use]
3842
pub fn bytes(&self) -> &[u8] {
3943
self.0.as_slice()
4044
}
45+
46+
/// Return content byte size
47+
#[must_use]
48+
pub fn len(&self) -> usize {
49+
self.0.len()
50+
}
51+
52+
/// Return `true` if content is empty
53+
#[must_use]
54+
pub fn is_empty(&self) -> bool {
55+
self.0.is_empty()
56+
}
4157
}

rust/signed_doc/src/lib.rs

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,14 @@ use std::{
1414

1515
use anyhow::anyhow;
1616
pub use builder::Builder;
17-
use content::Content;
17+
pub use content::Content;
1818
use coset::{CborSerializable, Header};
1919
pub use metadata::{AdditionalFields, DocumentRef, Metadata, UuidV4, UuidV7};
2020
pub use minicbor::{decode, encode, Decode, Decoder, Encode};
21-
pub use signature::KidUri;
22-
use signature::Signatures;
21+
pub use signature::{KidUri, Signatures};
2322

2423
/// Inner type that holds the Catalyst Signed Document with parsing errors.
24+
#[derive(Debug, Clone)]
2525
struct InnerCatalystSignedDocument {
2626
/// Document Metadata
2727
metadata: Metadata,
@@ -37,17 +37,30 @@ struct InnerCatalystSignedDocument {
3737
/// non-optional.
3838
pub struct CatalystSignedDocument {
3939
/// Catalyst Signed Document metadata, raw doc, with content errors.
40-
pub(crate) inner: Arc<InnerCatalystSignedDocument>,
40+
inner: Arc<InnerCatalystSignedDocument>,
4141
}
4242

4343
impl Display for CatalystSignedDocument {
4444
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
4545
writeln!(f, "{}", self.inner.metadata)?;
46-
writeln!(f, "Signature Information [")?;
47-
for kid in &self.inner.signatures.kids() {
48-
writeln!(f, " {kid}")?;
46+
writeln!(f, "Payload Size: {} bytes", self.inner.content.len())?;
47+
writeln!(f, "Signature Information")?;
48+
if self.inner.signatures.0.is_empty() {
49+
writeln!(f, " This document is unsigned.")?;
50+
} else {
51+
for kid in &self.inner.signatures.kids() {
52+
writeln!(f, " Signature Key ID: {kid}")?;
53+
}
54+
}
55+
Ok(())
56+
}
57+
}
58+
59+
impl From<InnerCatalystSignedDocument> for CatalystSignedDocument {
60+
fn from(inner: InnerCatalystSignedDocument) -> Self {
61+
Self {
62+
inner: inner.into(),
4963
}
50-
writeln!(f, "]\n")
5164
}
5265
}
5366

@@ -154,22 +167,28 @@ impl Decode<'_, ()> for CatalystSignedDocument {
154167

155168
impl Encode<()> for CatalystSignedDocument {
156169
fn encode<W: minicbor::encode::Write>(
157-
&self, e: &mut encode::Encoder<W>, ctx: &mut (),
170+
&self, e: &mut encode::Encoder<W>, _ctx: &mut (),
158171
) -> Result<(), encode::Error<W::Error>> {
159172
let protected_header = Header::try_from(&self.inner.metadata).map_err(|e| {
160173
minicbor::encode::Error::message(format!("Failed to encode Document Metadata: {e}"))
161174
})?;
175+
162176
let mut builder = coset::CoseSignBuilder::new()
163177
.protected(protected_header)
164178
.payload(self.inner.content.bytes().to_vec());
179+
165180
for signature in self.signatures().signatures() {
166181
builder = builder.add_signature(signature);
167182
}
183+
168184
let cose_sign = builder.build();
185+
169186
let cose_bytes = cose_sign.to_vec().map_err(|e| {
170187
minicbor::encode::Error::message(format!("Failed to encode COSE Sign document: {e}"))
171188
})?;
172-
cose_bytes.encode(e, ctx)?;
173-
Ok(())
189+
190+
e.writer_mut()
191+
.write_all(&cose_bytes)
192+
.map_err(|_| minicbor::encode::Error::message("Failed to encode to CBOR"))
174193
}
175194
}

rust/signed_doc/src/metadata/additional_fields.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use super::{cose_protected_header_find, decode_cbor_uuid, encode_cbor_uuid, Docu
88
/// Additional Metadata Fields.
99
///
1010
/// These values are extracted from the COSE Sign protected header labels.
11-
#[derive(Default, Debug, serde::Serialize, serde::Deserialize)]
11+
#[derive(Clone, Default, Debug, serde::Serialize, serde::Deserialize)]
1212
pub struct AdditionalFields {
1313
/// Reference to the latest document.
1414
#[serde(rename = "ref")]

rust/signed_doc/src/metadata/content_encoding.rs

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,31 @@ pub enum ContentEncoding {
1414
Brotli,
1515
}
1616

17+
impl ContentEncoding {
18+
/// Compress a Brotli payload
19+
pub fn encode(self, mut payload: &[u8]) -> anyhow::Result<Vec<u8>> {
20+
match self {
21+
Self::Brotli => {
22+
let brotli_params = brotli::enc::BrotliEncoderParams::default();
23+
let mut buf = Vec::new();
24+
brotli::BrotliCompress(&mut payload, &mut buf, &brotli_params)?;
25+
Ok(buf)
26+
},
27+
}
28+
}
29+
30+
/// Decompress a Brotli payload
31+
pub fn decode(self, mut payload: &[u8]) -> anyhow::Result<Vec<u8>> {
32+
match self {
33+
Self::Brotli => {
34+
let mut buf = Vec::new();
35+
brotli::BrotliDecompress(&mut payload, &mut buf)?;
36+
Ok(buf)
37+
},
38+
}
39+
}
40+
}
41+
1742
impl Display for ContentEncoding {
1843
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
1944
match self {
@@ -53,16 +78,3 @@ impl TryFrom<&coset::cbor::Value> for ContentEncoding {
5378
}
5479
}
5580
}
56-
57-
impl ContentEncoding {
58-
/// Decompress a Brotli payload
59-
pub fn decode(self, mut payload: &[u8]) -> anyhow::Result<Vec<u8>> {
60-
match self {
61-
Self::Brotli => {
62-
let mut buf = Vec::new();
63-
brotli::BrotliDecompress(&mut payload, &mut buf)?;
64-
Ok(buf)
65-
},
66-
}
67-
}
68-
}

rust/signed_doc/src/metadata/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ const CONTENT_ENCODING_KEY: &str = "Content-Encoding";
2626
/// Document Metadata.
2727
///
2828
/// These values are extracted from the COSE Sign protected header.
29-
#[derive(Debug, serde::Deserialize)]
29+
#[derive(Clone, Debug, serde::Deserialize)]
3030
pub struct Metadata {
3131
/// Document Type `UUIDv4`.
3232
#[serde(rename = "type")]

0 commit comments

Comments
 (0)