Skip to content

Commit ae80c50

Browse files
feat(rust/signed-doc): Catalyst Signed Documents signature kid validation rule (#274)
* add SignatureKidRule * feat(rust/signed-doc): update tests with signature kid validation * wip * add unit test * wip --------- Co-authored-by: Joaquín Rosales <[email protected]>
1 parent 41ea811 commit ae80c50

File tree

9 files changed

+260
-125
lines changed

9 files changed

+260
-125
lines changed

rust/catalyst-types/src/id_uri/role_index.rs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,19 +24,23 @@ pub enum RoleIndexError {
2424
pub struct RoleIndex(u16);
2525

2626
impl RoleIndex {
27-
/// Default Role Index
28-
pub const DEFAULT: RoleIndex = RoleIndex(0);
27+
/// Delegated Representative
28+
pub const DREP: RoleIndex = RoleIndex(1);
29+
/// Proposer
30+
pub const PROPOSER: RoleIndex = RoleIndex(3);
31+
/// Default Role 0
32+
pub const ROLE_0: RoleIndex = RoleIndex(0);
2933

30-
/// Is the `RoleIndex` the default value
34+
/// Is the `RoleIndex` the default value (Role 0)
3135
#[must_use]
3236
pub fn is_default(self) -> bool {
33-
self == Self::DEFAULT
37+
self == Self::ROLE_0
3438
}
3539
}
3640

3741
impl Default for RoleIndex {
3842
fn default() -> Self {
39-
Self::DEFAULT
43+
Self::ROLE_0
4044
}
4145
}
4246

rust/signed_doc/src/validator/mod.rs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,15 @@ use std::{
1010
};
1111

1212
use anyhow::Context;
13-
use catalyst_types::{id_uri::IdUri, problem_report::ProblemReport, uuid::Uuid};
13+
use catalyst_types::{
14+
id_uri::{role_index::RoleIndex, IdUri},
15+
problem_report::ProblemReport,
16+
uuid::Uuid,
17+
};
1418
use coset::{CoseSign, CoseSignature};
1519
use rules::{
1620
CategoryRule, ContentEncodingRule, ContentTypeRule, RefRule, ReplyRule, Rules, SectionRule,
17-
TemplateRule,
21+
SignatureKidRule, TemplateRule,
1822
};
1923

2024
use crate::{
@@ -51,6 +55,9 @@ fn document_rules_init() -> HashMap<Uuid, Rules> {
5155
doc_ref: RefRule::NotSpecified,
5256
reply: ReplyRule::NotSpecified,
5357
section: SectionRule::NotSpecified,
58+
kid: SignatureKidRule {
59+
exp: &[RoleIndex::PROPOSER],
60+
},
5461
};
5562
document_rules_map.insert(PROPOSAL_DOCUMENT_UUID_TYPE, proposal_document_rules);
5663

@@ -81,6 +88,9 @@ fn document_rules_init() -> HashMap<Uuid, Rules> {
8188
},
8289
section: SectionRule::Specified { optional: true },
8390
category: CategoryRule::NotSpecified,
91+
kid: SignatureKidRule {
92+
exp: &[RoleIndex::ROLE_0],
93+
},
8494
};
8595
document_rules_map.insert(COMMENT_DOCUMENT_UUID_TYPE, comment_document_rules);
8696

@@ -102,6 +112,9 @@ fn document_rules_init() -> HashMap<Uuid, Rules> {
102112
},
103113
reply: ReplyRule::NotSpecified,
104114
section: SectionRule::NotSpecified,
115+
kid: SignatureKidRule {
116+
exp: &[RoleIndex::PROPOSER],
117+
},
105118
};
106119

107120
document_rules_map.insert(

rust/signed_doc/src/validator/rules/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ mod content_type;
1111
mod doc_ref;
1212
mod reply;
1313
mod section;
14+
mod signature_kid;
1415
mod template;
1516

1617
pub(crate) use category::CategoryRule;
@@ -19,6 +20,7 @@ pub(crate) use content_type::ContentTypeRule;
1920
pub(crate) use doc_ref::RefRule;
2021
pub(crate) use reply::ReplyRule;
2122
pub(crate) use section::SectionRule;
23+
pub(crate) use signature_kid::SignatureKidRule;
2224
pub(crate) use template::TemplateRule;
2325

2426
/// Struct represented a full collection of rules for all fields
@@ -37,6 +39,8 @@ pub(crate) struct Rules {
3739
pub(crate) section: SectionRule,
3840
/// 'category' field validation rule
3941
pub(crate) category: CategoryRule,
42+
/// `kid` field validation rule
43+
pub(crate) kid: SignatureKidRule,
4044
}
4145

4246
impl Rules {
@@ -53,6 +57,7 @@ impl Rules {
5357
self.reply.check(doc, provider).boxed(),
5458
self.section.check(doc).boxed(),
5559
self.category.check(doc, provider).boxed(),
60+
self.kid.check(doc).boxed(),
5661
];
5762

5863
let res = futures::future::join_all(rules)
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
//! Catalyst Signed Document COSE signature `kid` (Catalyst Id) role validation
2+
3+
use catalyst_types::id_uri::role_index::RoleIndex;
4+
5+
use crate::CatalystSignedDocument;
6+
7+
/// COSE signature `kid` (Catalyst Id) role validation
8+
pub(crate) struct SignatureKidRule {
9+
/// expected `RoleIndex` values for the `kid` field
10+
pub(crate) exp: &'static [RoleIndex],
11+
}
12+
13+
impl SignatureKidRule {
14+
/// Field validation rule
15+
#[allow(clippy::unused_async)]
16+
pub(crate) async fn check(&self, doc: &CatalystSignedDocument) -> anyhow::Result<bool> {
17+
let contains_exp_role = doc.kids().iter().enumerate().all(|(i, kid)| {
18+
let (role_index, _) = kid.role_and_rotation();
19+
let res = self.exp.contains(&role_index);
20+
if !res {
21+
doc.report().invalid_value(
22+
"kid",
23+
role_index.to_string().as_str(),
24+
format!("{:?}", self.exp).as_str(),
25+
format!(
26+
"Invalid Catalyst Signed Document signature {i} `kid` Catalyst Role value"
27+
)
28+
.as_str(),
29+
);
30+
}
31+
res
32+
});
33+
if !contains_exp_role {
34+
return Ok(false);
35+
}
36+
37+
Ok(true)
38+
}
39+
}
40+
41+
#[cfg(test)]
42+
mod tests {
43+
use catalyst_types::{
44+
id_uri::IdUri,
45+
uuid::{UuidV4, UuidV7},
46+
};
47+
48+
use super::*;
49+
use crate::{Builder, ContentType};
50+
51+
#[tokio::test]
52+
async fn signature_kid_rule_test() {
53+
let mut rule = SignatureKidRule {
54+
exp: &[RoleIndex::ROLE_0],
55+
};
56+
57+
let sk = ed25519_dalek::SigningKey::generate(&mut rand::rngs::OsRng);
58+
let pk = sk.verifying_key();
59+
let kid = IdUri::new("cardano", None, pk).with_role(RoleIndex::ROLE_0);
60+
61+
let doc = Builder::new()
62+
.with_decoded_content(serde_json::to_vec(&serde_json::Value::Null).unwrap())
63+
.with_json_metadata(serde_json::json!({
64+
"type": UuidV4::new().to_string(),
65+
"id": UuidV7::new().to_string(),
66+
"ver": UuidV7::new().to_string(),
67+
"content-type": ContentType::Json.to_string(),
68+
}))
69+
.unwrap()
70+
.add_signature(sk.to_bytes(), kid)
71+
.unwrap()
72+
.build();
73+
74+
assert!(rule.check(&doc).await.unwrap());
75+
76+
rule.exp = &[RoleIndex::PROPOSER];
77+
assert!(!rule.check(&doc).await.unwrap());
78+
}
79+
}

rust/signed_doc/tests/comment.rs

Lines changed: 57 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//! Integration test for comment document validation part.
22
33
use catalyst_signed_doc::{providers::tests::TestCatalystSignedDocumentProvider, *};
4+
use catalyst_types::id_uri::role_index::RoleIndex;
45

56
mod common;
67

@@ -12,21 +13,24 @@ async fn test_valid_comment_doc() {
1213
common::create_dummy_doc(doc_types::COMMENT_TEMPLATE_UUID_TYPE).unwrap();
1314

1415
let uuid_v7 = UuidV7::new();
15-
let (doc, ..) = common::create_dummy_signed_doc(Some(serde_json::json!({
16-
"content-type": ContentType::Json.to_string(),
17-
"content-encoding": ContentEncoding::Brotli.to_string(),
18-
"type": doc_types::COMMENT_DOCUMENT_UUID_TYPE,
19-
"id": uuid_v7.to_string(),
20-
"ver": uuid_v7.to_string(),
21-
"template": {
22-
"id": template_doc_id,
23-
"ver": template_doc_ver
24-
},
25-
"ref": {
26-
"id": proposal_doc_id,
27-
"ver": proposal_doc_ver
28-
}
29-
})))
16+
let (doc, ..) = common::create_dummy_signed_doc(
17+
Some(serde_json::json!({
18+
"content-type": ContentType::Json.to_string(),
19+
"content-encoding": ContentEncoding::Brotli.to_string(),
20+
"type": doc_types::COMMENT_DOCUMENT_UUID_TYPE,
21+
"id": uuid_v7.to_string(),
22+
"ver": uuid_v7.to_string(),
23+
"template": {
24+
"id": template_doc_id,
25+
"ver": template_doc_ver
26+
},
27+
"ref": {
28+
"id": proposal_doc_id,
29+
"ver": proposal_doc_ver
30+
}
31+
})),
32+
RoleIndex::ROLE_0,
33+
)
3034
.unwrap();
3135

3236
let mut provider = TestCatalystSignedDocumentProvider::default();
@@ -66,25 +70,28 @@ async fn test_valid_comment_doc_with_reply() {
6670
.build();
6771

6872
let uuid_v7 = UuidV7::new();
69-
let (doc, ..) = common::create_dummy_signed_doc(Some(serde_json::json!({
70-
"content-type": ContentType::Json.to_string(),
71-
"content-encoding": ContentEncoding::Brotli.to_string(),
72-
"type": doc_types::COMMENT_DOCUMENT_UUID_TYPE,
73-
"id": uuid_v7.to_string(),
74-
"ver": uuid_v7.to_string(),
75-
"template": {
76-
"id": template_doc_id,
77-
"ver": template_doc_ver
78-
},
79-
"ref": {
80-
"id": proposal_doc_id,
81-
"ver": proposal_doc_ver
82-
},
83-
"reply": {
84-
"id": comment_doc_id,
85-
"ver": comment_doc_ver
86-
}
87-
})))
73+
let (doc, ..) = common::create_dummy_signed_doc(
74+
Some(serde_json::json!({
75+
"content-type": ContentType::Json.to_string(),
76+
"content-encoding": ContentEncoding::Brotli.to_string(),
77+
"type": doc_types::COMMENT_DOCUMENT_UUID_TYPE,
78+
"id": uuid_v7.to_string(),
79+
"ver": uuid_v7.to_string(),
80+
"template": {
81+
"id": template_doc_id,
82+
"ver": template_doc_ver
83+
},
84+
"ref": {
85+
"id": proposal_doc_id,
86+
"ver": proposal_doc_ver
87+
},
88+
"reply": {
89+
"id": comment_doc_id,
90+
"ver": comment_doc_ver
91+
}
92+
})),
93+
RoleIndex::ROLE_0,
94+
)
8895
.unwrap();
8996

9097
let mut provider = TestCatalystSignedDocumentProvider::default();
@@ -105,19 +112,22 @@ async fn test_invalid_comment_doc() {
105112
common::create_dummy_doc(doc_types::COMMENT_TEMPLATE_UUID_TYPE).unwrap();
106113

107114
let uuid_v7 = UuidV7::new();
108-
let (doc, ..) = common::create_dummy_signed_doc(Some(serde_json::json!({
109-
"content-type": ContentType::Json.to_string(),
110-
"content-encoding": ContentEncoding::Brotli.to_string(),
111-
"type": doc_types::COMMENT_DOCUMENT_UUID_TYPE,
112-
"id": uuid_v7.to_string(),
113-
"ver": uuid_v7.to_string(),
114-
"template": {
115-
"id": template_doc_id,
116-
"ver": template_doc_ver
117-
},
118-
// without ref
119-
"ref": serde_json::Value::Null
120-
})))
115+
let (doc, ..) = common::create_dummy_signed_doc(
116+
Some(serde_json::json!({
117+
"content-type": ContentType::Json.to_string(),
118+
"content-encoding": ContentEncoding::Brotli.to_string(),
119+
"type": doc_types::COMMENT_DOCUMENT_UUID_TYPE,
120+
"id": uuid_v7.to_string(),
121+
"ver": uuid_v7.to_string(),
122+
"template": {
123+
"id": template_doc_id,
124+
"ver": template_doc_ver
125+
},
126+
// without ref
127+
"ref": serde_json::Value::Null
128+
})),
129+
RoleIndex::ROLE_0,
130+
)
121131
.unwrap();
122132

123133
let mut provider = TestCatalystSignedDocumentProvider::default();

rust/signed_doc/tests/common/mod.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
use std::str::FromStr;
44

55
use catalyst_signed_doc::*;
6+
use catalyst_types::id_uri::role_index::RoleIndex;
67

78
pub fn test_metadata() -> (UuidV7, UuidV4, serde_json::Value) {
89
let uuid_v7 = UuidV7::new();
@@ -28,15 +29,17 @@ pub fn test_metadata() -> (UuidV7, UuidV4, serde_json::Value) {
2829
(uuid_v7, uuid_v4, metadata_fields)
2930
}
3031

31-
pub fn create_dummy_key_pair() -> anyhow::Result<(
32+
pub fn create_dummy_key_pair(
33+
role_index: RoleIndex,
34+
) -> anyhow::Result<(
3235
ed25519_dalek::SigningKey,
3336
ed25519_dalek::VerifyingKey,
3437
IdUri,
3538
)> {
3639
let sk = create_signing_key();
3740
let pk = sk.verifying_key();
3841
let kid = IdUri::from_str(&format!(
39-
"id.catalyst://cardano/{}/0/0",
42+
"id.catalyst://cardano/{}/{role_index}/0",
4043
base64_url::encode(pk.as_bytes())
4144
))?;
4245

@@ -71,9 +74,9 @@ pub fn create_signing_key() -> ed25519_dalek::SigningKey {
7174
}
7275

7376
pub fn create_dummy_signed_doc(
74-
with_metadata: Option<serde_json::Value>,
77+
with_metadata: Option<serde_json::Value>, with_role_index: RoleIndex,
7578
) -> anyhow::Result<(CatalystSignedDocument, ed25519_dalek::VerifyingKey, IdUri)> {
76-
let (sk, pk, kid) = create_dummy_key_pair()?;
79+
let (sk, pk, kid) = create_dummy_key_pair(with_role_index)?;
7780

7881
let content = serde_json::to_vec(&serde_json::Value::Null)?;
7982
let (_, _, metadata) = test_metadata();

0 commit comments

Comments
 (0)