Skip to content

Commit 2a2aea1

Browse files
committed
feat(rust/signed-doc): add types for Metadata fields.
* add content_encoding and content_type fields to Metadata * update the example for making signed docs
1 parent 5cd299f commit 2a2aea1

File tree

11 files changed

+296
-80
lines changed

11 files changed

+296
-80
lines changed

rust/signed_doc/examples/mk_signed_doc.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ use std::{
66
fs::{read_to_string, File},
77
io::{Read, Write},
88
path::PathBuf,
9-
str::FromStr,
109
};
1110

1211
use clap::Parser;
@@ -58,7 +57,7 @@ enum Cli {
5857
},
5958
}
6059

61-
const CONTENT_ENCODING_KEY: &str = "content encoding";
60+
const CONTENT_ENCODING_KEY: &str = "Content-Encoding";
6261
const CONTENT_ENCODING_VALUE: &str = "br";
6362
const UUID_CBOR_TAG: u64 = 37;
6463

@@ -120,7 +119,8 @@ impl Cli {
120119
} => {
121120
let doc_schema = load_schema_from_file(&schema)?;
122121
let json_doc = load_json_from_file(&doc)?;
123-
let json_meta = load_json_from_file(&meta)?;
122+
let json_meta = load_json_from_file(&meta)
123+
.map_err(|e| anyhow::anyhow!("Failed to load metadata from file: {e}"))?;
124124
validate_json(&json_doc, &doc_schema)?;
125125
let compressed_doc = brotli_compress_json(&json_doc)?;
126126
let empty_cose_sign = build_empty_cose_doc(compressed_doc, &json_meta);
@@ -299,14 +299,13 @@ fn validate_cose(
299299
validate_json(&json_doc, schema)?;
300300

301301
for sign in &cose.signatures {
302-
let key_id = sign.protected.header.key_id.clone();
302+
let key_id = &sign.protected.header.key_id;
303303
anyhow::ensure!(
304304
!key_id.is_empty(),
305305
"COSE missing signature protected header `kid` field "
306306
);
307307

308-
let kid_str = String::from_utf8_lossy(&key_id);
309-
let kid = Kid::from_str(&kid_str)?;
308+
let kid = Kid::try_from(key_id.as_ref())?;
310309
println!("Signature Key ID: {kid}");
311310
let data_to_sign = cose.tbs_data(&[], sign);
312311
let signature_bytes = sign.signature.as_slice().try_into().map_err(|_| {
@@ -338,12 +337,13 @@ fn validate_cose_protected_header(cose: &coset::CoseSign) -> anyhow::Result<()>
338337
cose.protected.header.content_type == expected_header.content_type,
339338
"Invalid COSE document protected header `content-type` field"
340339
);
340+
println!("HEADER REST: \n{:?}", cose.protected.header.rest);
341341
anyhow::ensure!(
342342
cose.protected.header.rest.iter().any(|(key, value)| {
343343
key == &coset::Label::Text(CONTENT_ENCODING_KEY.to_string())
344344
&& value == &coset::cbor::Value::Text(CONTENT_ENCODING_VALUE.to_string())
345345
}),
346-
"Invalid COSE document protected header {CONTENT_ENCODING_KEY} field"
346+
"Invalid COSE document protected header"
347347
);
348348

349349
let Some((_, value)) = cose

rust/signed_doc/src/lib.rs

Lines changed: 11 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,6 @@ mod signature;
1414
pub use metadata::{DocumentRef, Metadata, UuidV7};
1515
pub use signature::Kid;
1616

17-
/// Catalyst Signed Document Content Encoding Key.
18-
const CONTENT_ENCODING_KEY: &str = "Content-Encoding";
19-
/// Catalyst Signed Document Content Encoding Value.
20-
const CONTENT_ENCODING_VALUE: &str = "br";
21-
2217
/// Keep all the contents private.
2318
/// Better even to use a structure like this. Wrapping in an Arc means we don't have to
2419
/// manage the Arc anywhere else. These are likely to be large, best to have the Arc be
@@ -77,37 +72,23 @@ impl TryFrom<Vec<u8>> for CatalystSignedDocument {
7772
let cose = coset::CoseSign::from_tagged_slice(&cose_bytes)
7873
.or(coset::CoseSign::from_slice(&cose_bytes))
7974
.map_err(|e| anyhow::anyhow!("Invalid COSE Sign document: {e}"))?;
80-
let mut content_errors = Vec::new();
81-
let expected_header = cose_protected_header();
8275

83-
if cose.protected.header.content_type != expected_header.content_type {
84-
content_errors
85-
.push("Invalid COSE document protected header `content-type` field".to_string());
86-
}
76+
let mut content_errors = Vec::new();
8777

88-
if !cose.protected.header.rest.iter().any(|(key, value)| {
89-
key == &coset::Label::Text(CONTENT_ENCODING_KEY.to_string())
90-
&& value == &coset::cbor::Value::Text(CONTENT_ENCODING_VALUE.to_string())
91-
}) {
92-
content_errors.push(
93-
"Invalid COSE document protected header {CONTENT_ENCODING_KEY} field".to_string(),
94-
);
95-
}
9678
let metadata = Metadata::from(&cose.protected);
79+
9780
if metadata.has_error() {
9881
content_errors.extend_from_slice(metadata.content_errors());
9982
}
100-
let payload = match &cose.payload {
101-
Some(payload) => {
102-
let mut buf = Vec::new();
103-
let mut bytes = payload.as_slice();
104-
brotli::BrotliDecompress(&mut bytes, &mut buf)?;
105-
serde_json::from_slice(&buf)?
106-
},
107-
None => {
108-
println!("COSE missing payload field with the JSON content in it");
109-
serde_json::Value::Object(serde_json::Map::new())
110-
},
83+
84+
let payload = if let Some(payload) = &cose.payload {
85+
let mut buf = Vec::new();
86+
let mut bytes = payload.as_slice();
87+
brotli::BrotliDecompress(&mut bytes, &mut buf)?;
88+
serde_json::from_slice(&buf)?
89+
} else {
90+
println!("COSE missing payload field with the JSON content in it");
91+
serde_json::Value::Object(serde_json::Map::new())
11192
};
11293
let signatures = cose.signatures.clone();
11394
let inner = InnerCatalystSignedDocument {
@@ -173,27 +154,5 @@ impl CatalystSignedDocument {
173154
pub fn doc_section(&self) -> Option<String> {
174155
self.inner.metadata.doc_section()
175156
}
176-
}
177-
178-
/// Generate the COSE protected header used by Catalyst Signed Document.
179-
fn cose_protected_header() -> coset::Header {
180-
coset::HeaderBuilder::new()
181-
.content_format(coset::iana::CoapContentFormat::Json)
182-
.text_value(
183-
CONTENT_ENCODING_KEY.to_string(),
184-
CONTENT_ENCODING_VALUE.to_string().into(),
185-
)
186-
.build()
187-
}
188157

189-
/// Find a value for a given key in the protected header.
190-
fn cose_protected_header_find(
191-
cose: &coset::CoseSign, rest_key: &str,
192-
) -> Option<coset::cbor::Value> {
193-
cose.protected
194-
.header
195-
.rest
196-
.iter()
197-
.find(|(key, _)| key == &coset::Label::Text(rest_key.to_string()))
198-
.map(|(_, value)| value.clone())
199158
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
//! Document Payload Content Encoding.
2+
3+
/// Catalyst Signed Document Content Encoding Key.
4+
const CONTENT_ENCODING_KEY: &str = "Content-Encoding";
5+
6+
/// IANA `CoAP` Content Encoding.
7+
#[derive(Debug, serde::Deserialize)]
8+
#[serde(untagged)]
9+
pub enum ContentEncoding {
10+
/// Brotli compression.format.
11+
#[serde(rename = "br")]
12+
Brotli,
13+
}
14+
15+
impl TryFrom<&coset::cbor::Value> for ContentEncoding {
16+
type Error = anyhow::Error;
17+
18+
#[allow(clippy::todo)]
19+
fn try_from(val: &coset::cbor::Value) -> anyhow::Result<ContentEncoding> {
20+
match val.as_text() {
21+
Some(encoding) => {
22+
match encoding.to_string().to_lowercase().as_ref() {
23+
"br" => Ok(ContentEncoding::Brotli),
24+
_ => anyhow::bail!("Unsupported Content Encoding: {encoding}"),
25+
}
26+
},
27+
_ => {
28+
anyhow::bail!("Expected Content Encoding to be a string");
29+
},
30+
}
31+
}
32+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
//! Document Payload Content Type.
2+
3+
/// Payload Content Type.
4+
#[derive(Debug, serde::Deserialize)]
5+
#[serde(untagged, rename_all_fields = "lowercase")]
6+
pub enum ContentType {
7+
/// 'application/cbor'
8+
Cbor,
9+
/// 'application/json'
10+
Json,
11+
}
12+
13+
impl TryFrom<&coset::ContentType> for ContentType {
14+
type Error = anyhow::Error;
15+
16+
fn try_from(value: &coset::ContentType) -> Result<Self, Self::Error> {
17+
use coset::iana::CoapContentFormat as Format;
18+
match value {
19+
coset::ContentType::Assigned(Format::Json) => Ok(ContentType::Json),
20+
coset::ContentType::Assigned(Format::Cbor) => Ok(ContentType::Cbor),
21+
_ => anyhow::bail!("Unsupported Content Type {value:?}"),
22+
}
23+
}
24+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
//! Document ID.
2+
use std::fmt::{Display, Formatter};
3+
4+
use super::UuidV7;
5+
6+
/// Catalyst Document ID.
7+
#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, serde::Deserialize)]
8+
#[serde(from = "UuidV7")]
9+
pub struct DocumentId {
10+
/// Inner UUID type
11+
uuid: UuidV7,
12+
}
13+
14+
impl DocumentId {
15+
/// Generates a zeroed out `UUIDv7` that can never be valid.
16+
pub fn invalid() -> Self {
17+
Self {
18+
uuid: UuidV7::invalid(),
19+
}
20+
}
21+
22+
/// Check if this is a valid `UUIDv7`.
23+
pub fn is_valid(&self) -> bool {
24+
self.uuid.is_valid()
25+
}
26+
27+
/// Returns the `uuid::Uuid` type.
28+
#[must_use]
29+
pub fn uuid(&self) -> uuid::Uuid {
30+
self.uuid.uuid()
31+
}
32+
}
33+
34+
impl Display for DocumentId {
35+
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
36+
write!(f, "{}", self.uuid)
37+
}
38+
}
39+
40+
impl From<UuidV7> for DocumentId {
41+
fn from(uuid: UuidV7) -> Self {
42+
Self { uuid }
43+
}
44+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
//! Document Type.
2+
use std::fmt::{Display, Formatter};
3+
4+
use super::UuidV4;
5+
6+
/// Catalyst Document Type.
7+
#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, serde::Deserialize)]
8+
#[serde(from = "UuidV4")]
9+
pub struct DocumentType(UuidV4);
10+
11+
impl DocumentType {
12+
/// Generates a zeroed out `UUIDv4` that can never be valid.
13+
pub fn invalid() -> Self {
14+
Self(UuidV4::invalid())
15+
}
16+
17+
/// Check if this is a valid `UUIDv4`.
18+
pub fn is_valid(&self) -> bool {
19+
self.0.is_valid()
20+
}
21+
22+
/// Returns the `uuid::Uuid` type.
23+
#[must_use]
24+
pub fn uuid(&self) -> uuid::Uuid {
25+
self.0.uuid()
26+
}
27+
}
28+
29+
impl Display for DocumentType {
30+
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
31+
write!(f, "{}", self.0)
32+
}
33+
}
34+
35+
impl From<UuidV4> for DocumentType {
36+
fn from(value: UuidV4) -> Self {
37+
Self(value)
38+
}
39+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
//! Document Version.
2+
use std::fmt::{Display, Formatter};
3+
4+
use super::UuidV7;
5+
6+
/// Catalyst Document Version.
7+
#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, serde::Deserialize)]
8+
pub struct DocumentVersion(UuidV7);
9+
10+
impl DocumentVersion {
11+
/// Generates a zeroed out `UUIDv7` that can never be valid.
12+
pub fn invalid() -> Self {
13+
Self(UuidV7::invalid())
14+
}
15+
16+
/// Check if this is a valid `UUIDv7`.
17+
pub fn is_valid(&self) -> bool {
18+
self.0.is_valid()
19+
}
20+
21+
/// Returns the `uuid::Uuid` type.
22+
#[must_use]
23+
pub fn uuid(&self) -> uuid::Uuid {
24+
self.0.uuid()
25+
}
26+
}
27+
28+
impl Display for DocumentVersion {
29+
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
30+
write!(f, "{}", self.0)
31+
}
32+
}
33+
34+
impl From<UuidV7> for DocumentVersion {
35+
fn from(value: UuidV7) -> Self {
36+
Self(value)
37+
}
38+
}

0 commit comments

Comments
 (0)