Skip to content
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
4e5ff19
fix(rust/signed-doc): add content type validation
saibatizoku Jan 27, 2025
15741c6
fix(rust/signed-doc): content encoding field is optional
saibatizoku Jan 27, 2025
3d34e47
fix(rust/signed-doc): add 'half' feature from minicbor crate
saibatizoku Jan 27, 2025
3a66559
wip(rust/signed_doc): implement verification method
saibatizoku Jan 28, 2025
70440b5
fix(rust/signed-doc): fix content-type verification
saibatizoku Jan 28, 2025
bd4561d
wip(rust/signed_doc): implement verification method
saibatizoku Jan 28, 2025
2392ed5
wip(rust/signed_doc): implement verification method
saibatizoku Jan 28, 2025
dd60045
Merge branch 'main' into feat/doc-signing-logic
saibatizoku Jan 28, 2025
22c46f0
feat(rust/signed-doc): add signatures to Catalyst Signed Document
saibatizoku Jan 28, 2025
0f85057
fix(rust/signed-doc): refactor cli tool to sign
saibatizoku Jan 29, 2025
ee0657c
fix(rust/signed-doc): add verify command to cli tool
saibatizoku Jan 29, 2025
262b1ad
fix(rust/catalyst-types): refactor sigining logic into builder
saibatizoku Jan 29, 2025
a1c11ea
Merge branch 'main' into feat/doc-signing-logic
saibatizoku Jan 29, 2025
56b0509
chore(docs): fix spelling
saibatizoku Jan 29, 2025
70cae9f
fix(rust/signed-doc): cleanup
saibatizoku Jan 31, 2025
0329541
fix content validation, add unit test
Mr-Leshiy Feb 2, 2025
3d1b044
remove clippy
Mr-Leshiy Feb 2, 2025
5682dd5
fix
Mr-Leshiy Feb 2, 2025
a2af6f1
feat(rust/signed-doc): test verify signatures
saibatizoku Feb 2, 2025
ba6eb18
fix(rust/signed-doc): update catalyst-types tag
saibatizoku Feb 3, 2025
6bf6274
fix(rust/signed-doc): update KID to use catalyst-id type
saibatizoku Feb 3, 2025
ae94111
fix(rust/signed-doc): simpler test closure
saibatizoku Feb 3, 2025
0897cd6
feat(rust/signed-doc): add kids and authors methods for signed documents
saibatizoku Feb 3, 2025
2cc67e1
fix(rust/signed-doc): use rbac-registration type for public key and a…
saibatizoku Feb 3, 2025
ad99056
chore(docs): fix markdown
saibatizoku Feb 3, 2025
55edc1b
fix(rust/signed-doc): add more assertions to signature verification
saibatizoku Feb 3, 2025
9c54aa2
Merge remote-tracking branch 'origin/main' into feat/use-catalyst-id
saibatizoku Feb 3, 2025
90880b6
fix(rust/signed-doc): code cleanup
saibatizoku Feb 3, 2025
013c3bd
Merge branch 'main' into feat/use-catalyst-id
saibatizoku Feb 4, 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
8 changes: 5 additions & 3 deletions rust/signed_doc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@ license.workspace = true
workspace = true

[dependencies]
catalyst-types = { version = "0.0.1", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250124-00" }
rbac-registration = { version = "0.0.2", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250128-01" }
catalyst-types = { version = "0.0.1", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250128-01" }
anyhow = "1.0.95"
serde = { version = "1.0.217", features = ["derive"] }
serde_json = "1.0.134"
coset = "0.3.8"
minicbor = "0.25.1"
minicbor = { version = "0.25.1", features = ["half"] }
brotli = "7.0.0"
ed25519-dalek = { version = "2.1.1", features = ["pem", "rand_core"] }
uuid = { version = "1.11.0", features = ["v4", "v7", "serde"] }
Expand All @@ -26,9 +27,10 @@ clap = { version = "4.5.23", features = ["derive", "env"] }


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


[[bin]]
name = "signed-docs"
path = "examples/mk_signed_doc.rs"
path = "examples/mk_signed_doc.rs"
15 changes: 12 additions & 3 deletions rust/signed_doc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,29 @@ Catalyst signed document crate implementation based on this

## Example

Generate a `ed25519` private and public keys
### Generate a `ed25519` private and public keys

```shell
openssl genpkey -algorithm=ED25519 -out=private.pem -outpubkey=public.pem
```

Prepare non-signed document,
### Prepare non-signed document

`meta.json` file should follow the [`meta.schema.json`](./meta.schema.json).

```shell
cargo run -p catalyst-signed-doc --example mk_signed_doc build signed_doc/doc.json signed_doc/doc.cose signed_doc/meta.json
```

Inspect document
### Sign document

`KID` is a valid Catalyst ID URI.

```shell
cargo run -p catalyst-signed-doc --example mk_signed_doc sign signed_doc/doc.cose signed_doc/meta.json <KID>
```

### Inspect document

```shell
cargo run -p catalyst-signed-doc --example mk_signed_doc inspect signed_doc/doc.cose
Expand Down
125 changes: 68 additions & 57 deletions rust/signed_doc/examples/mk_signed_doc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,9 @@ use std::{
path::PathBuf,
};

use catalyst_signed_doc::{Builder, CatalystSignedDocument, KidUri, Metadata};
use catalyst_signed_doc::{Builder, CatalystSignedDocument, IdUri, Metadata, SimplePublicKeyType};
use clap::Parser;
use coset::CborSerializable;
use ed25519_dalek::{ed25519::signature::Signer, pkcs8::DecodePrivateKey};
use ed25519_dalek::pkcs8::{DecodePrivateKey, DecodePublicKey};

fn main() {
if let Err(err) = Cli::parse().exec() {
Expand Down Expand Up @@ -40,7 +39,7 @@ enum Cli {
/// Path to the secret key in PEM format
sk: PathBuf,
/// Signer kid
kid: KidUri,
kid: IdUri,
},
/// Inspects Catalyst Signed Document
Inspect {
Expand All @@ -52,6 +51,16 @@ 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 @@ -68,53 +77,80 @@ impl Cli {
let payload = serde_json::to_vec(&json_doc)?;
// Start with no signatures.
let signed_doc = Builder::new()
.with_content(payload)
.with_decoded_content(payload)
.with_metadata(metadata)
.build()?;
let mut bytes: Vec<u8> = Vec::new();
minicbor::encode(signed_doc, &mut bytes)
.map_err(|e| anyhow::anyhow!("Failed to encode document: {e}"))?;

write_bytes_to_file(&bytes, &output)?;
save_signed_doc(signed_doc, &output)?;
},
Self::Sign { sk, doc, kid } => {
let sk = load_secret_key_from_file(&sk)
.map_err(|e| anyhow::anyhow!("Failed to load SK FILE: {e}"))?;
let mut cose = load_cose_from_file(&doc)
.map_err(|e| anyhow::anyhow!("Failed to load COSE FROM FILE: {e}"))?;
add_signature_to_cose(&mut cose, &sk, kid.to_string());
store_cose_file(cose, &doc)?;
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 new_signed_doc = builder.add_signature(sk.to_bytes(), kid)?.build()?;
save_signed_doc(new_signed_doc, &doc)?;
},
Self::Inspect { path } => {
let mut cose_file = File::open(path)?;
let mut cose_bytes = Vec::new();
cose_file.read_to_end(&mut cose_bytes)?;
decode_signed_doc(&cose_bytes);
let cose_bytes = read_bytes_from_file(&path)?;
inspect_signed_doc(&cose_bytes)?;
},
Self::InspectBytes { cose_sign_hex } => {
let cose_bytes = hex::decode(&cose_sign_hex)?;
decode_signed_doc(&cose_bytes);
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(())
}
}

fn decode_signed_doc(cose_bytes: &[u8]) {
fn read_bytes_from_file(path: &PathBuf) -> anyhow::Result<Vec<u8>> {
let mut cose_file = File::open(path)?;
let mut cose_bytes = Vec::new();
cose_file.read_to_end(&mut cose_bytes)?;
Ok(cose_bytes)
}

fn inspect_signed_doc(cose_bytes: &[u8]) -> anyhow::Result<()> {
println!(
"Decoding {} bytes: {}",
"Decoding {} bytes:\n{}",
cose_bytes.len(),
hex::encode(cose_bytes)
);
let cat_signed_doc = signed_doc_from_bytes(cose_bytes)?;
println!("This is a valid Catalyst Document.");
println!("{cat_signed_doc}");
Ok(())
}

match CatalystSignedDocument::try_from(cose_bytes) {
Ok(cat_signed_doc) => {
println!("This is a valid Catalyst Document.");
println!("{cat_signed_doc}");
},
Err(e) => eprintln!("Invalid Catalyst Document, err: {e}"),
}
fn save_signed_doc(signed_doc: CatalystSignedDocument, path: &PathBuf) -> anyhow::Result<()> {
let mut bytes: Vec<u8> = Vec::new();
minicbor::encode(signed_doc, &mut bytes)
.map_err(|e| anyhow::anyhow!("Failed to encode document: {e}"))?;

write_bytes_to_file(&bytes, path)
}

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}"))
}

fn load_json_from_file<T>(path: &PathBuf) -> anyhow::Result<T>
Expand All @@ -124,45 +160,20 @@ where T: for<'de> serde::Deserialize<'de> {
Ok(json)
}

fn load_cose_from_file(cose_path: &PathBuf) -> anyhow::Result<coset::CoseSign> {
let cose_file_bytes = read_bytes_from_file(cose_path)?;
let cose = coset::CoseSign::from_slice(&cose_file_bytes).map_err(|e| anyhow::anyhow!("{e}"))?;
Ok(cose)
}

fn read_bytes_from_file(path: &PathBuf) -> anyhow::Result<Vec<u8>> {
let mut file_bytes = Vec::new();
File::open(path)?.read_to_end(&mut file_bytes)?;
Ok(file_bytes)
}

fn write_bytes_to_file(bytes: &[u8], output: &PathBuf) -> anyhow::Result<()> {
File::create(output)?
.write_all(bytes)
.map_err(|e| anyhow::anyhow!("Failed to write to file {output:?}: {e}"))
}

fn store_cose_file(cose: coset::CoseSign, output: &PathBuf) -> anyhow::Result<()> {
let cose_bytes = cose
.to_vec()
.map_err(|e| anyhow::anyhow!("Failed to Store COSE SIGN: {e}"))?;
write_bytes_to_file(&cose_bytes, output)
}

fn load_secret_key_from_file(sk_path: &PathBuf) -> anyhow::Result<ed25519_dalek::SigningKey> {
let sk_str = read_to_string(sk_path)?;
let sk = ed25519_dalek::SigningKey::from_pkcs8_pem(&sk_str)?;
Ok(sk)
}

fn add_signature_to_cose(cose: &mut coset::CoseSign, sk: &ed25519_dalek::SigningKey, kid: String) {
let protected_header = coset::HeaderBuilder::new()
.key_id(kid.into_bytes())
.algorithm(coset::iana::Algorithm::EdDSA);
let mut signature = coset::CoseSignatureBuilder::new()
.protected(protected_header.build())
.build();
let data_to_sign = cose.tbs_data(&[], &signature);
signature.signature = sk.sign(&data_to_sign).to_vec();
cose.signatures.push(signature);
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)
}
54 changes: 51 additions & 3 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 ed25519_dalek::{ed25519::signature::Signer, SecretKey};

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

/// Catalyst Signed Document Builder.
#[derive(Debug, Default)]
#[derive(Debug, Default, Clone)]
pub struct Builder {
/// Document Metadata
metadata: Option<Metadata>,
Expand All @@ -26,13 +29,58 @@ impl Builder {
self
}

/// Set document content
/// Set decoded (original) document content bytes
#[must_use]
pub fn with_content(mut self, content: Vec<u8>) -> Self {
pub fn with_decoded_content(mut self, content: Vec<u8>) -> Self {
self.content = Some(content);
self
}

/// Set document signatures
#[must_use]
pub fn with_signatures(mut self, signatures: Signatures) -> Self {
self.signatures = signatures;
self
}

/// Add a signature to the document
///
/// # Errors
///
/// Fails if a `CatalystSignedDocument` cannot be created due to missing metadata or
/// content, due to malformed data, or when the signed document cannot be
/// converted into `coset::CoseSign`.
pub fn add_signature(self, sk: SecretKey, kid: IdUri) -> anyhow::Result<Self> {
let cose_sign = self
.clone()
.build()
.map_err(|e| anyhow::anyhow!("Failed to sign: {e}"))?
.as_cose_sign()
.map_err(|e| anyhow::anyhow!("Failed to sign: {e}"))?;
let Self {
metadata: Some(metadata),
content: Some(content),
mut signatures,
} = self
else {
anyhow::bail!("Metadata and Content are needed for signing");
};
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());
let mut signature = coset::CoseSignatureBuilder::new()
.protected(protected_header.build())
.build();
let data_to_sign = cose_sign.tbs_data(&[], &signature);
signature.signature = sk.sign(&data_to_sign).to_vec();
signatures.push(kid, signature);
Ok(Self::new()
.with_decoded_content(content)
.with_metadata(metadata)
.with_signatures(signatures))
}

/// Build a signed document
///
/// ## Errors
Expand Down
12 changes: 3 additions & 9 deletions rust/signed_doc/src/content.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ impl Content {
.decode(&data)
.map_err(|e| anyhow::anyhow!("Failed to decode {encoding} content: {e}"))?;
}
content_type.validate(&data)?;

Ok(Self {
data,
Expand All @@ -41,11 +42,10 @@ impl Content {
///
/// # Errors
/// Returns an error if content is not correctly encoded
#[allow(clippy::unnecessary_wraps)]
pub(crate) fn from_decoded(
data: Vec<u8>, content_type: ContentType, content_encoding: Option<ContentEncoding>,
) -> anyhow::Result<Self> {
// TODO add content_type verification
content_type.validate(&data)?;
Ok(Self {
data,
content_type,
Expand Down Expand Up @@ -87,13 +87,7 @@ impl Content {

/// Return content byte size
#[must_use]
pub fn len(&self) -> usize {
pub fn size(&self) -> usize {
self.data.len()
}

/// Return `true` if content is empty
#[must_use]
pub fn is_empty(&self) -> bool {
self.data.is_empty()
}
}
Loading