diff --git a/rust/signed_doc/examples/cat-signed-doc.rs b/rust/signed_doc/examples/cat-signed-doc.rs index 381f72852d..20fe9c2b1d 100644 --- a/rust/signed_doc/examples/cat-signed-doc.rs +++ b/rust/signed_doc/examples/cat-signed-doc.rs @@ -11,8 +11,8 @@ use std::{ path::PathBuf, }; +use catalyst_signed_doc::CatalystSignedDocument; use clap::Parser; -use signed_doc::CatalystSignedDocument; /// Hermes cli commands #[derive(clap::Parser)] @@ -42,7 +42,7 @@ impl Cli { Self::InspectBytes { cose_sign_str } => hex::decode(&cose_sign_str)?, }; println!("Bytes read:\n{}\n", hex::encode(&cose_bytes)); - let cat_signed_doc: CatalystSignedDocument = cose_bytes.try_into()?; + let cat_signed_doc: CatalystSignedDocument = cose_bytes.as_slice().try_into()?; println!("{cat_signed_doc}"); Ok(()) } diff --git a/rust/signed_doc/examples/mk_signed_doc.rs b/rust/signed_doc/examples/mk_signed_doc.rs index 88fea415ad..1074d55e94 100644 --- a/rust/signed_doc/examples/mk_signed_doc.rs +++ b/rust/signed_doc/examples/mk_signed_doc.rs @@ -8,13 +8,13 @@ use std::{ path::PathBuf, }; +use catalyst_signed_doc::{DocumentRef, KidUri, Metadata, UuidV7}; use clap::Parser; use coset::{iana::CoapContentFormat, CborSerializable}; use ed25519_dalek::{ ed25519::signature::Signer, pkcs8::{DecodePrivateKey, DecodePublicKey}, }; -use signed_doc::{DocumentRef, KidUri, Metadata, UuidV7}; fn main() { if let Err(err) = Cli::parse().exec() { diff --git a/rust/signed_doc/src/error.rs b/rust/signed_doc/src/error.rs new file mode 100644 index 0000000000..a33e4176b4 --- /dev/null +++ b/rust/signed_doc/src/error.rs @@ -0,0 +1,19 @@ +//! Catalyst Signed Document errors. + +/// Catalyst Signed Document error. +#[derive(thiserror::Error, Debug)] +#[error("Catalyst Signed Document Error: {0:#?}")] +pub struct Error(pub(crate) Vec); + +impl From> for Error { + fn from(e: Vec) -> Self { + Error(e) + } +} + +impl Error { + /// List of errors. + pub fn errors(&self) -> &Vec { + &self.0 + } +} diff --git a/rust/signed_doc/src/lib.rs b/rust/signed_doc/src/lib.rs index 8c3d3d2afb..5f1c17b086 100644 --- a/rust/signed_doc/src/lib.rs +++ b/rust/signed_doc/src/lib.rs @@ -6,8 +6,9 @@ use std::{ }; use anyhow::anyhow; -use coset::CborSerializable; +use coset::{CborSerializable, CoseSignature}; +mod error; mod metadata; mod payload; mod signature; @@ -15,6 +16,7 @@ mod signature; pub use metadata::{DocumentRef, Metadata, UuidV7}; use payload::JsonContent; pub use signature::KidUri; +use signature::Signatures; /// Inner type that holds the Catalyst Signed Document with parsing errors. #[derive(Default)] @@ -23,6 +25,8 @@ struct InnerCatalystSignedDocument { metadata: Metadata, /// Document Payload viewed as JSON Content payload: JsonContent, + /// Signatures + signatures: Signatures, /// Raw COSE Sign data cose_sign: coset::CoseSign, } @@ -54,7 +58,7 @@ impl Display for CatalystSignedDocument { } impl TryFrom<&[u8]> for CatalystSignedDocument { - type Error = Vec; + type Error = error::Error; fn try_from(cose_bytes: &[u8]) -> Result { // Try reading as a tagged COSE SIGN, otherwise try reading as untagged. @@ -67,7 +71,7 @@ impl TryFrom<&[u8]> for CatalystSignedDocument { let mut payload = JsonContent::default(); if let Some(bytes) = &cose_sign.payload { - match JsonContent::try_from((bytes, metadata.content_encoding())) { + match JsonContent::try_from((bytes.as_ref(), metadata.content_encoding())) { Ok(c) => payload = c, Err(e) => { content_errors.push(anyhow!("Invalid Payload: {e}")); @@ -77,11 +81,27 @@ impl TryFrom<&[u8]> for CatalystSignedDocument { content_errors.push(anyhow!("COSE payload is empty")); }; + let mut signatures = Signatures::default(); + match Signatures::try_from(&cose_sign.signatures) { + Ok(s) => signatures = s, + Err(errors) => { + for e in errors.errors() { + content_errors.push(anyhow!("{e}")); + } + }, + } + let inner = InnerCatalystSignedDocument { metadata, payload, + signatures, cose_sign, }; + + if !content_errors.is_empty() { + return Err(error::Error(content_errors)); + } + Ok(CatalystSignedDocument { inner: Arc::new(inner), }) @@ -132,4 +152,22 @@ impl CatalystSignedDocument { pub fn doc_section(&self) -> Option { self.inner.metadata.doc_section() } + + /// Return Raw COSE SIGN bytes. + #[must_use] + pub fn cose_sign_bytes(&self) -> Vec { + self.inner.cose_sign.clone().to_vec().unwrap_or_default() + } + + /// Return a list of signature KIDs. + #[must_use] + pub fn signature_kids(&self) -> Vec { + self.inner.signatures.kids() + } + + /// Return a list of signatures. + #[must_use] + pub fn signatures(&self) -> Vec { + self.inner.signatures.signatures() + } } diff --git a/rust/signed_doc/src/metadata/mod.rs b/rust/signed_doc/src/metadata/mod.rs index 433e6ddb97..67bc5d02dd 100644 --- a/rust/signed_doc/src/metadata/mod.rs +++ b/rust/signed_doc/src/metadata/mod.rs @@ -49,10 +49,10 @@ pub struct Metadata { } impl Metadata { - /// Are there any validation errors (as opposed to structural errors). + /// Returns true if metadata has no validation errors. #[must_use] pub fn is_valid(&self) -> bool { - !self.content_errors.is_empty() + self.content_errors.is_empty() } /// Return Document Type `UUIDv4`. @@ -144,7 +144,7 @@ impl Default for Metadata { } impl TryFrom<&coset::ProtectedHeader> for Metadata { - type Error = Vec; + type Error = crate::error::Error; #[allow(clippy::too_many_lines)] fn try_from(protected: &coset::ProtectedHeader) -> Result { @@ -254,7 +254,7 @@ impl TryFrom<&coset::ProtectedHeader> for Metadata { if errors.is_empty() { Ok(metadata) } else { - Err(errors) + Err(errors.into()) } } } diff --git a/rust/signed_doc/src/payload/json.rs b/rust/signed_doc/src/payload/json.rs index 51be619351..e5dc349673 100644 --- a/rust/signed_doc/src/payload/json.rs +++ b/rust/signed_doc/src/payload/json.rs @@ -20,22 +20,19 @@ impl TryFrom<&[u8]> for Content { } } -impl TryFrom<(&Vec, Option)> for Content { +impl TryFrom<(&[u8], Option)> for Content { type Error = anyhow::Error; - fn try_from( - (value, encoding): (&Vec, Option), - ) -> Result { + fn try_from((value, encoding): (&[u8], Option)) -> Result { if let Some(content_encoding) = encoding { - match content_encoding.decode(value) { - Ok(decompressed) => { - return Self::try_from(decompressed.as_slice()); - }, + match content_encoding.decode(&value.to_vec()) { + Ok(decompressed) => Self::try_from(decompressed.as_slice()), Err(e) => { - anyhow::bail!("Failed to decode {encoding:?}: {e}"); + anyhow::bail!("Failed to decode {encoding:?} content: {e}"); }, } + } else { + Self::try_from(value) } - Self::try_from(value.as_ref()) } } diff --git a/rust/signed_doc/src/signature/mod.rs b/rust/signed_doc/src/signature/mod.rs index 9ce4d71191..b60d18e30d 100644 --- a/rust/signed_doc/src/signature/mod.rs +++ b/rust/signed_doc/src/signature/mod.rs @@ -1,2 +1,57 @@ //! Catalyst Signed Document COSE Signature information. + pub use catalyst_types::kid_uri::KidUri; +use coset::CoseSignature; + +/// Catalyst Signed Document COSE Signature. +#[derive(Debug)] +pub struct Signature { + /// Key ID + kid: KidUri, + /// COSE Signature + signature: CoseSignature, +} + +/// List of Signatures. +#[derive(Default)] +pub struct Signatures(Vec); + +impl Signatures { + /// List of signature Key IDs. + pub fn kids(&self) -> Vec { + self.0.iter().map(|sig| sig.kid.clone()).collect() + } + + /// List of signatures. + pub fn signatures(&self) -> Vec { + self.0.iter().map(|sig| sig.signature.clone()).collect() + } +} + +impl TryFrom<&Vec> for Signatures { + type Error = crate::error::Error; + + fn try_from(value: &Vec) -> Result { + let mut signatures = Vec::new(); + let mut errors = Vec::new(); + value + .iter() + .cloned() + .enumerate() + .for_each(|(idx, signature)| { + match KidUri::try_from(signature.protected.header.key_id.as_ref()) { + Ok(kid) => signatures.push(Signature { kid, signature }), + Err(e) => { + errors.push(anyhow::anyhow!( + "Signature at index {idx} has valid Catalyst Key Id: {e}" + )); + }, + } + }); + if errors.is_empty() { + Err(errors.into()) + } else { + Ok(Signatures(signatures)) + } + } +}