Skip to content
Merged
Show file tree
Hide file tree
Changes from 38 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
7014694
wip(rust/signed_doc): add comment document type
saibatizoku Feb 4, 2025
25107e6
Merge remote-tracking branch 'origin/main' into feat/cat-sign-doc-val…
saibatizoku Feb 12, 2025
c2a8d7b
wip(rust/signed_doc): add comment document validation
saibatizoku Feb 13, 2025
1b0cbb5
Merge remote-tracking branch 'origin/main' into feat/cat-sign-doc-val…
saibatizoku Feb 13, 2025
8b9622b
chore(docs): fix spelling
saibatizoku Feb 13, 2025
5bb6cc4
fix(rust/signed-doc): signed docs implement Clone
saibatizoku Feb 13, 2025
b5d70cb
Merge remote-tracking branch 'origin/main' into feat/cat-sign-doc-val…
saibatizoku Feb 13, 2025
c6f3a04
Merge branch 'main' into feat/cat-sign-doc-validator
Mr-Leshiy Feb 16, 2025
1ce8a3c
add proposal document validation
Mr-Leshiy Feb 16, 2025
b54623a
refactor
Mr-Leshiy Feb 16, 2025
d710304
wip
Mr-Leshiy Feb 16, 2025
49b541e
add full template validation
Mr-Leshiy Feb 16, 2025
d173bf4
wip
Mr-Leshiy Feb 16, 2025
f9ec1fc
replace trait with Fn
Mr-Leshiy Feb 16, 2025
6630c02
wip
Mr-Leshiy Feb 16, 2025
2182445
wip
Mr-Leshiy Feb 17, 2025
cd4301c
wip
Mr-Leshiy Feb 17, 2025
40478ae
make signed doc signature validation async
Mr-Leshiy Feb 17, 2025
c287e3a
make a separate trait
Mr-Leshiy Feb 17, 2025
fc8d5ba
make all validation async
Mr-Leshiy Feb 17, 2025
3c9506b
Merge branch 'main' into feat/cat-sign-doc-validator
Mr-Leshiy Feb 17, 2025
2c2bd38
fix
Mr-Leshiy Feb 17, 2025
fb9e256
refactor
Mr-Leshiy Feb 18, 2025
613a958
add more rules
Mr-Leshiy Feb 18, 2025
bfd27c0
add section rule
Mr-Leshiy Feb 18, 2025
7efe2e2
cleanup
Mr-Leshiy Feb 18, 2025
09e1b2c
refactor
Mr-Leshiy Feb 18, 2025
6d933fb
added new Section struct
Mr-Leshiy Feb 19, 2025
20929de
wip
Mr-Leshiy Feb 19, 2025
d60579c
add utils mod
Mr-Leshiy Feb 19, 2025
38104bb
wip
Mr-Leshiy Feb 19, 2025
249f0b4
fix signature test
Mr-Leshiy Feb 19, 2025
40c82d2
refactor
Mr-Leshiy Feb 19, 2025
b7bbe15
finilize validation logic
Mr-Leshiy Feb 19, 2025
8121f33
Merge branch 'main' into feat/cat-sign-doc-validator
Mr-Leshiy Feb 20, 2025
5b42d74
fix spelling
Mr-Leshiy Feb 20, 2025
f2273d3
fix clippy lints
Mr-Leshiy Feb 20, 2025
0dece66
Merge branch 'main' into feat/cat-sign-doc-validator
Mr-Leshiy Feb 21, 2025
75e0a65
fix
Mr-Leshiy Feb 21, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions rust/signed_doc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,26 @@ license.workspace = true
workspace = true

[dependencies]
rbac-registration = { version = "0.0.4", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250220-00" }
catalyst-types = { version = "0.0.3", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250220-00" }
anyhow = "1.0.95"
serde = { version = "1.0.217", features = ["derive"] }
serde_json = "1.0.134"
coset = "0.3.8"
minicbor = { version = "0.25.1", features = ["half"] }
brotli = "7.0.0"
ed25519-dalek = { version = "2.1.1", features = ["pem", "rand_core"] }
ed25519-dalek = { version = "2.1.1" }
hex = "0.4.3"
strum = { version = "0.26.3", features = ["derive"] }
clap = { version = "4.5.23", features = ["derive", "env"] }

clap = { version = "4.5.23", features = ["derive", "env"] }
jsonschema = "0.28.3"
jsonpath-rust = "0.7.5"
futures = "0.3.31"

[dev-dependencies]
base64-url = "3.0.0"
rand = "0.8.5"

tokio = { version = "1.42.0", features = [ "macros" ] }
ed25519-dalek = { version = "2.1.1", features = ["rand_core"] }

[[bin]]
name = "signed-docs"
Expand Down
41 changes: 4 additions & 37 deletions rust/signed_doc/examples/mk_signed_doc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ use std::{
path::PathBuf,
};

use catalyst_signed_doc::{Builder, CatalystSignedDocument, IdUri, Metadata, SimplePublicKeyType};
use catalyst_signed_doc::{Builder, CatalystSignedDocument, IdUri, Metadata};
use clap::Parser;
use ed25519_dalek::pkcs8::{DecodePrivateKey, DecodePublicKey};
use ed25519_dalek::pkcs8::DecodePrivateKey;

fn main() {
if let Err(err) = Cli::parse().exec() {
Expand Down Expand Up @@ -51,16 +51,6 @@ enum Cli {
/// Hex-formatted COSE SIGN Bytes
cose_sign_hex: String,
},
/// Validates a signature by Key ID and verifying key
Verify {
/// Path to the formed (could be empty, without any signatures) COSE document
/// This exact file would be modified and new signature would be added
path: PathBuf,
/// Path to the verifying key in PEM format
pk: PathBuf,
/// Signer kid
kid: IdUri,
},
}

impl Cli {
Expand All @@ -87,7 +77,7 @@ impl Cli {
.map_err(|e| anyhow::anyhow!("Failed to load SK FILE: {e}"))?;
let cose_bytes = read_bytes_from_file(&doc)?;
let signed_doc = signed_doc_from_bytes(cose_bytes.as_slice())?;
let builder = signed_doc.into_builder();
let builder = signed_doc.into_builder()?;
let new_signed_doc = builder.add_signature(sk.to_bytes(), kid)?.build()?;
save_signed_doc(new_signed_doc, &doc)?;
},
Expand All @@ -99,22 +89,6 @@ impl Cli {
let cose_bytes = hex::decode(&cose_sign_hex)?;
inspect_signed_doc(&cose_bytes)?;
},
Self::Verify { path, pk, kid } => {
let pk = load_public_key_from_file(&pk)
.map_err(|e| anyhow::anyhow!("Failed to load PK FILE {pk:?}: {e}"))?;
let cose_bytes = read_bytes_from_file(&path)?;
let signed_doc = signed_doc_from_bytes(cose_bytes.as_slice())?;
signed_doc
.verify(|k| {
if k.to_string() == kid.to_string() {
SimplePublicKeyType::Ed25519(pk)
} else {
SimplePublicKeyType::Undefined
}
})
.map_err(|e| anyhow::anyhow!("Catalyst Document Verification failed: {e}"))?;
println!("Catalyst Signed Document is Verified.");
},
}
println!("Done");
Ok(())
Expand Down Expand Up @@ -149,8 +123,7 @@ fn save_signed_doc(signed_doc: CatalystSignedDocument, path: &PathBuf) -> anyhow
}

fn signed_doc_from_bytes(cose_bytes: &[u8]) -> anyhow::Result<CatalystSignedDocument> {
CatalystSignedDocument::try_from(cose_bytes)
.map_err(|e| anyhow::anyhow!("Invalid Catalyst Document: {e}"))
minicbor::decode(cose_bytes).map_err(|e| anyhow::anyhow!("Invalid Catalyst Document: {e}"))
}

fn load_json_from_file<T>(path: &PathBuf) -> anyhow::Result<T>
Expand All @@ -171,9 +144,3 @@ fn load_secret_key_from_file(sk_path: &PathBuf) -> anyhow::Result<ed25519_dalek:
let sk = ed25519_dalek::SigningKey::from_pkcs8_pem(&sk_str)?;
Ok(sk)
}

fn load_public_key_from_file(pk_path: &PathBuf) -> anyhow::Result<ed25519_dalek::VerifyingKey> {
let pk_str = read_to_string(pk_path)?;
let pk = ed25519_dalek::VerifyingKey::from_public_key_pem(&pk_str)?;
Ok(pk)
}
30 changes: 21 additions & 9 deletions rust/signed_doc/src/builder.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
//! Catalyst Signed Document Builder.
use catalyst_types::id_uri::IdUri;
use catalyst_types::{id_uri::IdUri, problem_report::ProblemReport};
use ed25519_dalek::{ed25519::signature::Signer, SecretKey};

use crate::{CatalystSignedDocument, Content, InnerCatalystSignedDocument, Metadata, Signatures};
use crate::{
CatalystSignedDocument, Content, InnerCatalystSignedDocument, Metadata, Signatures,
PROBLEM_REPORT_CTX,
};

/// Catalyst Signed Document Builder.
#[derive(Debug, Default, Clone)]
Expand All @@ -23,12 +26,24 @@ impl Builder {
}

/// Set document metadata
///
/// # Errors
/// - Fails if it is invalid metadata JSON object.
#[must_use]
pub fn with_metadata(mut self, metadata: Metadata) -> Self {
self.metadata = Some(metadata);
self
}

/// Set document metadata in JSON format
///
/// # Errors
/// - Fails if it is invalid metadata JSON object.
pub fn with_json_metadata(mut self, json: serde_json::Value) -> anyhow::Result<Self> {
self.metadata = Some(serde_json::from_value(json)?);
Ok(self)
}

/// Set decoded (original) document content bytes
#[must_use]
pub fn with_decoded_content(mut self, content: Vec<u8>) -> Self {
Expand Down Expand Up @@ -68,7 +83,7 @@ impl Builder {
let sk = ed25519_dalek::SigningKey::from_bytes(&sk);
let protected_header = coset::HeaderBuilder::new()
.key_id(kid.to_string().into_bytes())
.algorithm(metadata.algorithm().into());
.algorithm(metadata.algorithm()?.into());
let mut signature = coset::CoseSignatureBuilder::new()
.protected(protected_header.build())
.build();
Expand All @@ -94,17 +109,14 @@ impl Builder {
anyhow::bail!("Failed to build Catalyst Signed Document, missing document's content");
};
let signatures = self.signatures;
let content = Content::from_decoded(content, metadata.content_type()?)?;

let content = Content::from_decoded(
content,
metadata.content_type(),
metadata.content_encoding(),
)?;

let empty_report = ProblemReport::new(PROBLEM_REPORT_CTX);
Ok(InnerCatalystSignedDocument {
metadata,
content,
signatures,
report: empty_report,
}
.into())
}
Expand Down
117 changes: 58 additions & 59 deletions rust/signed_doc/src/content.rs
Original file line number Diff line number Diff line change
@@ -1,93 +1,92 @@
//! Catalyst Signed Document Content Payload

use catalyst_types::problem_report::ProblemReport;

use crate::metadata::{ContentEncoding, ContentType};

/// Decompressed Document Content type bytes.
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone, PartialEq, Default)]
pub struct Content {
/// Content data bytes
data: Vec<u8>,
/// Content type
content_type: ContentType,
/// Content encoding
content_encoding: Option<ContentEncoding>,
/// Original Decompressed Document's data bytes
data: Option<Vec<u8>>,
}

impl Content {
/// Creates a new `Content` value, from the encoded data.
/// verifies a Document's content, that it is correctly encoded and it corresponds and
/// parsed to the specified type
///
/// # Errors
/// Returns an error if content is not correctly encoded
pub(crate) fn from_encoded(
mut data: Vec<u8>, content_type: ContentType, content_encoding: Option<ContentEncoding>,
) -> anyhow::Result<Self> {
if let Some(encoding) = content_encoding {
data = encoding
.decode(&data)
.map_err(|e| anyhow::anyhow!("Failed to decode {encoding} content: {e}"))?;
mut data: Vec<u8>, content_type: Option<ContentType>,
content_encoding: Option<ContentEncoding>, report: &ProblemReport,
) -> Self {
if let Some(content_encoding) = content_encoding {
if let Ok(decoded_data) = content_encoding.decode(&data) {
data = decoded_data;
} else {
report.invalid_value(
"payload",
&hex::encode(&data),
&format!("Invalid Document content, should {content_encoding} encodable"),
"Invalid Document content type.",
);
return Self::default();
}
}
if let Some(content_type) = content_type {
if content_type.validate(&data).is_err() {
report.invalid_value(
"payload",
&hex::encode(&data),
&format!("Invalid Document content type, should {content_type} encodable"),
"Invalid Document content type.",
);
return Self::default();
}
}
content_type.validate(&data)?;

Ok(Self {
data,
content_type,
content_encoding,
})
Self { data: Some(data) }
}

/// Creates a new `Content` value, from the decoded (original) data.
/// verifies that it corresponds and parsed to the specified type.
///
/// # Errors
/// Returns an error if content is not correctly encoded
pub(crate) fn from_decoded(
data: Vec<u8>, content_type: ContentType, content_encoding: Option<ContentEncoding>,
) -> anyhow::Result<Self> {
pub(crate) fn from_decoded(data: Vec<u8>, content_type: ContentType) -> anyhow::Result<Self> {
content_type.validate(&data)?;
Ok(Self {
data,
content_type,
content_encoding,
})
}

/// Return `true` if Document's content type is Json
#[must_use]
pub fn is_json(&self) -> bool {
matches!(self.content_type, ContentType::Json)
Ok(Self { data: Some(data) })
}

/// Return `true` if Document's content type is Json
#[must_use]
pub fn is_cbor(&self) -> bool {
matches!(self.content_type, ContentType::Cbor)
}

/// Return an decoded (original) content bytes,
/// by the corresponding `content_encoding` provided field.
#[must_use]
pub fn decoded_bytes(&self) -> &[u8] {
&self.data
/// Return an decoded (original) content bytes.
///
/// # Errors
/// - Missing Document content
pub fn decoded_bytes(&self) -> anyhow::Result<&[u8]> {
self.data
.as_deref()
.ok_or(anyhow::anyhow!("Missing Document content"))
}

/// Return an encoded content bytes,
/// by the corresponding `content_encoding` provided field
pub(crate) fn encoded_bytes(&self) -> anyhow::Result<Vec<u8>> {
if let Some(encoding) = self.content_encoding {
let data = encoding
.encode(&self.data)
.map_err(|e| anyhow::anyhow!("Failed to encode {encoding} content: {e}"))?;
Ok(data)
} else {
Ok(self.data.clone())
}
/// by the provided `content_encoding` provided field.
///
/// # Errors
/// - Missing Document content
/// - Failed to encode content.
pub(crate) fn encoded_bytes(
&self, content_encoding: ContentEncoding,
) -> anyhow::Result<Vec<u8>> {
let content = self.decoded_bytes()?;
let data = content_encoding
.encode(content)
.map_err(|e| anyhow::anyhow!("Failed to encode {content_encoding} content: {e}"))?;
Ok(data)
}

/// Return content byte size
/// Return content byte size.
/// If content is empty returns `0`.
#[must_use]
pub fn size(&self) -> usize {
self.data.len()
self.data.as_ref().map(Vec::len).unwrap_or_default()
}
}
Loading