Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
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
1 change: 1 addition & 0 deletions .config/dictionaries/project.dic
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ MPMC
msvc
Multiaddr
multiera
mutlisig
mypy
nanos
netkey
Expand Down
53 changes: 26 additions & 27 deletions rust/signed_doc/src/providers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ use ed25519_dalek::VerifyingKey;
use crate::{CatalystSignedDocument, DocumentRef};

/// `VerifyingKey` Provider trait
pub trait VerifyingKeyProvider {
pub trait VerifyingKeyProvider: Send + Sync {
/// Try to get `VerifyingKey`
fn try_get_key(
&self,
kid: &CatalystId,
) -> impl Future<Output = anyhow::Result<Option<VerifyingKey>>>;
) -> impl Future<Output = anyhow::Result<Option<VerifyingKey>>> + Send;
}

/// `CatalystSignedDocument` Provider trait
Expand Down Expand Up @@ -53,11 +53,16 @@ pub mod tests {
};
use crate::{DocLocator, DocumentRef};

/// Simple testing implementation of `CatalystSignedDocumentProvider`
/// Simple testing implementation of `CatalystSignedDocumentProvider`,
#[derive(Default, Debug)]
pub struct TestCatalystSignedDocumentProvider(HashMap<DocumentRef, CatalystSignedDocument>);
pub struct TestCatalystProvider {
/// For `CatalystSignedDocumentProvider`.
signed_doc: HashMap<DocumentRef, CatalystSignedDocument>,
/// For `VerifyingKeyProvider`.
verifying_key: HashMap<CatalystId, VerifyingKey>,
}

impl TestCatalystSignedDocumentProvider {
impl TestCatalystProvider {
/// Inserts document into the `TestCatalystSignedDocumentProvider` where
/// if document reference is provided use that value.
/// if not use the id and version of the provided doc.
Expand All @@ -71,29 +76,38 @@ pub mod tests {
doc: &CatalystSignedDocument,
) -> anyhow::Result<()> {
if let Some(dr) = doc_ref {
self.0.insert(dr, doc.clone());
self.signed_doc.insert(dr, doc.clone());
} else {
let dr = DocumentRef::new(doc.doc_id()?, doc.doc_ver()?, DocLocator::default());
self.0.insert(dr, doc.clone());
self.signed_doc.insert(dr, doc.clone());
}
Ok(())
}

/// Inserts public key into the `TestVerifyingKeyProvider`
pub fn add_pk(
&mut self,
kid: CatalystId,
pk: VerifyingKey,
) {
self.verifying_key.insert(kid, pk);
}
}

impl CatalystSignedDocumentProvider for TestCatalystSignedDocumentProvider {
impl CatalystSignedDocumentProvider for TestCatalystProvider {
async fn try_get_doc(
&self,
doc_ref: &DocumentRef,
) -> anyhow::Result<Option<CatalystSignedDocument>> {
Ok(self.0.get(doc_ref).cloned())
Ok(self.signed_doc.get(doc_ref).cloned())
}

async fn try_get_last_doc(
&self,
id: catalyst_types::uuid::UuidV7,
) -> anyhow::Result<Option<CatalystSignedDocument>> {
Ok(self
.0
.signed_doc
.iter()
.filter(|(doc_ref, _)| doc_ref.id() == &id)
.max_by_key(|(doc_ref, _)| doc_ref.ver().uuid())
Expand All @@ -109,27 +123,12 @@ pub mod tests {
}
}

/// Simple testing implementation of `VerifyingKeyProvider`
#[derive(Default)]
pub struct TestVerifyingKeyProvider(HashMap<CatalystId, VerifyingKey>);

impl TestVerifyingKeyProvider {
/// Inserts public key into the `TestVerifyingKeyProvider`
pub fn add_pk(
&mut self,
kid: CatalystId,
pk: VerifyingKey,
) {
self.0.insert(kid, pk);
}
}

impl VerifyingKeyProvider for TestVerifyingKeyProvider {
impl VerifyingKeyProvider for TestCatalystProvider {
async fn try_get_key(
&self,
kid: &CatalystId,
) -> anyhow::Result<Option<VerifyingKey>> {
Ok(self.0.get(kid).copied())
Ok(self.verifying_key.get(kid).copied())
}
}
}
86 changes: 6 additions & 80 deletions rust/signed_doc/src/validator/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ use std::{
sync::{Arc, LazyLock},
};

use anyhow::Context;
use catalyst_types::{catalyst_id::role_index::RoleId, problem_report::ProblemReport};
use catalyst_types::catalyst_id::role_index::RoleId;
use rules::{
ContentEncodingRule, ContentRule, ContentSchema, ContentTypeRule, IdRule, ParametersRule,
RefRule, ReplyRule, Rules, SectionRule, SignatureKidRule, VerRule,
Expand All @@ -22,7 +21,7 @@ use crate::{
},
metadata::DocType,
providers::{CatalystSignedDocumentProvider, VerifyingKeyProvider},
signature::{tbs_data, Signature},
validator::rules::SignatureRule,
CatalystSignedDocument, ContentEncoding, ContentType,
};

Expand Down Expand Up @@ -62,6 +61,7 @@ fn proposal_rule() -> Rules {
kid: SignatureKidRule {
exp: &[RoleId::Proposer],
},
signature: SignatureRule { mutlisig: true },
}
}

Expand Down Expand Up @@ -104,6 +104,7 @@ fn proposal_comment_rule() -> Rules {
kid: SignatureKidRule {
exp: &[RoleId::Role0],
},
signature: SignatureRule { mutlisig: true },
}
}

Expand Down Expand Up @@ -152,6 +153,7 @@ fn proposal_submission_action_rule() -> Rules {
kid: SignatureKidRule {
exp: &[RoleId::Proposer],
},
signature: SignatureRule { mutlisig: true },
}
}

Expand Down Expand Up @@ -185,7 +187,7 @@ pub async fn validate<Provider>(
provider: &Provider,
) -> anyhow::Result<bool>
where
Provider: CatalystSignedDocumentProvider,
Provider: CatalystSignedDocumentProvider + VerifyingKeyProvider,
{
let Ok(doc_type) = doc.doc_type() else {
doc.report().missing_field(
Expand All @@ -207,82 +209,6 @@ where
rules.check(doc, provider).await
}

/// Verify document signatures.
/// Return true if all signatures are valid, otherwise return false.
///
/// # Errors
/// If `provider` returns error, fails fast throwing that error.
pub async fn validate_signatures(
doc: &CatalystSignedDocument,
provider: &impl VerifyingKeyProvider,
) -> anyhow::Result<bool> {
if doc.signatures().is_empty() {
doc.report().other(
"Catalyst Signed Document is unsigned",
"During Catalyst Signed Document signature validation",
);
return Ok(false);
}

let sign_rules = doc
.signatures()
.iter()
.map(|sign| validate_signature(doc, sign, provider, doc.report()));

let res = futures::future::join_all(sign_rules)
.await
.into_iter()
.collect::<anyhow::Result<Vec<_>>>()?
.iter()
.all(|res| *res);

Ok(res)
}

/// A single signature validation function
async fn validate_signature<Provider>(
doc: &CatalystSignedDocument,
sign: &Signature,
provider: &Provider,
report: &ProblemReport,
) -> anyhow::Result<bool>
where
Provider: VerifyingKeyProvider,
{
let kid = sign.kid();

let Some(pk) = provider.try_get_key(kid).await? else {
report.other(
&format!("Missing public key for {kid}."),
"During public key extraction",
);
return Ok(false);
};

let tbs_data = tbs_data(kid, doc.doc_meta(), doc.content()).context("Probably a bug, cannot build CBOR COSE bytes for signature verification from the structurally valid COSE object.")?;

let Ok(signature_bytes) = sign.signature().try_into() else {
report.invalid_value(
"cose signature",
&format!("{}", sign.signature().len()),
&format!("must be {}", ed25519_dalek::Signature::BYTE_SIZE),
"During encoding cose signature to bytes",
);
return Ok(false);
};

let signature = ed25519_dalek::Signature::from_bytes(signature_bytes);
if pk.verify_strict(&tbs_data, &signature).is_err() {
report.functional_validation(
&format!("Verification failed for signature with Key ID {kid}"),
"During signature validation with verifying key",
);
return Ok(false);
}

Ok(true)
}

#[cfg(test)]
mod tests {
use crate::validator::document_rules_init;
Expand Down
17 changes: 7 additions & 10 deletions rust/signed_doc/src/validator/rules/doc_ref.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,8 +188,8 @@ mod tests {

use super::*;
use crate::{
builder::tests::Builder, metadata::SupportedField,
providers::tests::TestCatalystSignedDocumentProvider, DocLocator, DocumentRef,
builder::tests::Builder, metadata::SupportedField, providers::tests::TestCatalystProvider,
DocLocator, DocumentRef,
};

#[test_case(
Expand Down Expand Up @@ -397,12 +397,9 @@ mod tests {
)]
#[tokio::test]
async fn ref_specified_test(
doc_gen: impl FnOnce(
&[DocType; 2],
&mut TestCatalystSignedDocumentProvider,
) -> CatalystSignedDocument
doc_gen: impl FnOnce(&[DocType; 2], &mut TestCatalystProvider) -> CatalystSignedDocument
) -> bool {
let mut provider = TestCatalystSignedDocumentProvider::default();
let mut provider = TestCatalystProvider::default();

let exp_types: [DocType; 2] = [UuidV4::new().into(), UuidV4::new().into()];

Expand Down Expand Up @@ -430,7 +427,7 @@ mod tests {

#[tokio::test]
async fn ref_specified_optional_test() {
let provider = TestCatalystSignedDocumentProvider::default();
let provider = TestCatalystProvider::default();
let rule = RefRule::Specified {
exp_ref_types: vec![UuidV4::new().into()],
optional: true,
Expand All @@ -439,7 +436,7 @@ mod tests {
let doc = Builder::new().build();
assert!(rule.check(&doc, &provider).await.unwrap());

let provider = TestCatalystSignedDocumentProvider::default();
let provider = TestCatalystProvider::default();
let rule = RefRule::Specified {
exp_ref_types: vec![UuidV4::new().into()],
optional: false,
Expand All @@ -452,7 +449,7 @@ mod tests {
#[tokio::test]
async fn ref_rule_not_specified_test() {
let rule = RefRule::NotSpecified;
let provider = TestCatalystSignedDocumentProvider::default();
let provider = TestCatalystProvider::default();

let doc = Builder::new().build();
assert!(rule.check(&doc, &provider).await.unwrap());
Expand Down
8 changes: 4 additions & 4 deletions rust/signed_doc/src/validator/rules/id.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,8 @@ mod tests {

use super::*;
use crate::{
builder::tests::Builder, metadata::SupportedField,
providers::tests::TestCatalystSignedDocumentProvider, UuidV7,
builder::tests::Builder, metadata::SupportedField, providers::tests::TestCatalystProvider,
UuidV7,
};

#[test_case(
Expand Down Expand Up @@ -170,9 +170,9 @@ mod tests {
)]
#[tokio::test]
async fn id_test(
doc_gen: impl FnOnce(&TestCatalystSignedDocumentProvider) -> CatalystSignedDocument
doc_gen: impl FnOnce(&TestCatalystProvider) -> CatalystSignedDocument
) -> bool {
let provider = TestCatalystSignedDocumentProvider::default();
let provider = TestCatalystProvider::default();
let doc = doc_gen(&provider);

IdRule.check(&doc, &provider).await.unwrap()
Expand Down
13 changes: 11 additions & 2 deletions rust/signed_doc/src/validator/rules/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@

use futures::FutureExt;

use crate::{providers::CatalystSignedDocumentProvider, CatalystSignedDocument};
use crate::{
providers::{CatalystSignedDocumentProvider, VerifyingKeyProvider},
CatalystSignedDocument,
};

mod content_encoding;
mod content_type;
Expand All @@ -12,6 +15,7 @@ mod id;
mod parameters;
mod reply;
mod section;
mod signature;
mod signature_kid;
mod template;
mod ver;
Expand All @@ -23,6 +27,7 @@ pub(crate) use id::IdRule;
pub(crate) use parameters::ParametersRule;
pub(crate) use reply::ReplyRule;
pub(crate) use section::SectionRule;
pub(crate) use signature::SignatureRule;
pub(crate) use signature_kid::SignatureKidRule;
pub(crate) use template::{ContentRule, ContentSchema};
pub(crate) use ver::VerRule;
Expand All @@ -49,6 +54,8 @@ pub(crate) struct Rules {
pub(crate) parameters: ParametersRule,
/// `kid` field validation rule
pub(crate) kid: SignatureKidRule,
/// document's signatures validation rule
pub(crate) signature: SignatureRule,
}

impl Rules {
Expand All @@ -59,7 +66,7 @@ impl Rules {
provider: &Provider,
) -> anyhow::Result<bool>
where
Provider: CatalystSignedDocumentProvider,
Provider: CatalystSignedDocumentProvider + VerifyingKeyProvider,
{
let rules = [
self.id.check(doc, provider).boxed(),
Expand All @@ -72,6 +79,7 @@ impl Rules {
self.section.check(doc).boxed(),
self.parameters.check(doc, provider).boxed(),
self.kid.check(doc).boxed(),
self.signature.check(doc, provider).boxed(),
];

let res = futures::future::join_all(rules)
Expand All @@ -80,6 +88,7 @@ impl Rules {
.collect::<anyhow::Result<Vec<_>>>()?
.iter()
.all(|res| *res);

Ok(res)
}
}
Loading
Loading