Skip to main content

confium_crypto_zk/
threshold_abs.rs

1//! Threshold attribute-based signing.
2//!
3//! Sign if the signer's attributes satisfy a policy AND enough
4//! signers (threshold) participate. Combines attribute-based access
5//! control with threshold signing.
6
7use serde::{Deserialize, Serialize};
8
9/// An attribute value.
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
11pub enum Attribute {
12    Region(String),
13    Role(String),
14    Clearance(u32),
15    Department(String),
16}
17
18/// A signing policy: attributes that must be satisfied.
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct SignPolicy {
21    /// Required attributes (all must be present).
22    pub required: Vec<Attribute>,
23    /// Minimum number of qualifying signers.
24    pub threshold: u32,
25}
26
27/// A signer with attributes.
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct AttributeSigner {
30    pub id: String,
31    pub attributes: Vec<Attribute>,
32}
33
34/// Check if a signer satisfies the required attributes.
35pub fn satisfies_policy(signer: &AttributeSigner, policy: &SignPolicy) -> bool {
36    for req in &policy.required {
37        if !signer.attributes.contains(req) {
38            return false;
39        }
40    }
41    true
42}
43
44/// A signing session with attribute-based access control.
45#[derive(Debug)]
46pub struct AbsSession {
47    pub policy: SignPolicy,
48    pub signers: Vec<AttributeSigner>,
49    pub authorized: Vec<String>,
50}
51
52impl AbsSession {
53    pub fn new(policy: SignPolicy) -> Self {
54        Self {
55            policy,
56            signers: Vec::new(),
57            authorized: Vec::new(),
58        }
59    }
60
61    /// Add a signer. Returns true if they satisfy the policy.
62    pub fn add_signer(&mut self, signer: AttributeSigner) -> bool {
63        let authorized = satisfies_policy(&signer, &self.policy);
64        if authorized {
65            self.authorized.push(signer.id.clone());
66        }
67        self.signers.push(signer);
68        authorized
69    }
70
71    /// Can the session proceed? (enough authorized signers)
72    pub fn can_sign(&self) -> bool {
73        self.authorized.len() >= self.policy.threshold as usize
74    }
75
76    /// Number of authorized signers.
77    pub fn authorized_count(&self) -> usize {
78        self.authorized.len()
79    }
80
81    /// List authorized signer IDs.
82    pub fn authorized_ids(&self) -> &[String] {
83        &self.authorized
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90
91    fn make_policy() -> SignPolicy {
92        SignPolicy {
93            required: vec![
94                Attribute::Role("director".into()),
95                Attribute::Region("eu".into()),
96            ],
97            threshold: 2,
98        }
99    }
100
101    #[test]
102    fn satisfies_policy_all_attributes() {
103        let signer = AttributeSigner {
104            id: "alice".into(),
105            attributes: vec![
106                Attribute::Role("director".into()),
107                Attribute::Region("eu".into()),
108            ],
109        };
110        assert!(satisfies_policy(&signer, &make_policy()));
111    }
112
113    #[test]
114    fn missing_attribute_fails() {
115        let signer = AttributeSigner {
116            id: "bob".into(),
117            attributes: vec![Attribute::Role("director".into())],
118        };
119        assert!(!satisfies_policy(&signer, &make_policy()));
120    }
121
122    #[test]
123    fn session_threshold_met() {
124        let mut session = AbsSession::new(make_policy());
125        session.add_signer(AttributeSigner {
126            id: "alice".into(),
127            attributes: vec![
128                Attribute::Role("director".into()),
129                Attribute::Region("eu".into()),
130            ],
131        });
132        session.add_signer(AttributeSigner {
133            id: "bob".into(),
134            attributes: vec![
135                Attribute::Role("director".into()),
136                Attribute::Region("eu".into()),
137            ],
138        });
139        assert!(session.can_sign());
140        assert_eq!(session.authorized_count(), 2);
141    }
142
143    #[test]
144    fn session_threshold_not_met() {
145        let mut session = AbsSession::new(make_policy());
146        session.add_signer(AttributeSigner {
147            id: "alice".into(),
148            attributes: vec![
149                Attribute::Role("director".into()),
150                Attribute::Region("eu".into()),
151            ],
152        });
153        assert!(!session.can_sign());
154    }
155
156    #[test]
157    fn unauthorized_signers_not_counted() {
158        let mut session = AbsSession::new(make_policy());
159        session.add_signer(AttributeSigner {
160            id: "alice".into(),
161            attributes: vec![
162                Attribute::Role("director".into()),
163                Attribute::Region("eu".into()),
164            ],
165        });
166        session.add_signer(AttributeSigner {
167            id: "bob".into(),
168            attributes: vec![Attribute::Role("analyst".into())], // wrong role
169        });
170        assert!(!session.can_sign());
171        assert_eq!(session.authorized_count(), 1);
172    }
173
174    #[test]
175    fn clearance_attribute() {
176        let policy = SignPolicy {
177            required: vec![Attribute::Clearance(5)],
178            threshold: 1,
179        };
180        let signer = AttributeSigner {
181            id: "alice".into(),
182            attributes: vec![Attribute::Clearance(5)],
183        };
184        assert!(satisfies_policy(&signer, &policy));
185    }
186
187    #[test]
188    fn different_clearance_fails() {
189        let policy = SignPolicy {
190            required: vec![Attribute::Clearance(5)],
191            threshold: 1,
192        };
193        let signer = AttributeSigner {
194            id: "alice".into(),
195            attributes: vec![Attribute::Clearance(3)],
196        };
197        assert!(!satisfies_policy(&signer, &policy));
198    }
199
200    #[test]
201    fn authorized_ids_listed() {
202        let mut session = AbsSession::new(make_policy());
203        session.add_signer(AttributeSigner {
204            id: "alice".into(),
205            attributes: vec![
206                Attribute::Role("director".into()),
207                Attribute::Region("eu".into()),
208            ],
209        });
210        assert_eq!(session.authorized_ids(), &["alice"]);
211    }
212}