confium_tc_keys/
integrity.rs1use confium_tc_core::share_adapter::NormalizedShare;
9
10#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum IntegrityResult {
13 Valid,
15 Invalid(Vec<IntegrityIssue>),
17}
18
19#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
21pub enum IntegrityIssue {
22 #[error("scalar is zero")]
24 ZeroScalar,
25 #[error("scalar has wrong length: {actual} (expected 32)")]
27 ScalarLength { actual: usize },
28 #[error("scalar >= curve order")]
30 ScalarOutOfRange,
31 #[error("party_idx is 0 (must be >= 1)")]
33 PartyIdxZero,
34 #[error("party_idx {party_idx} > party_count {party_count}")]
36 PartyIdxExceedsCount {
37 party_idx: u32,
39 party_count: u32,
41 },
42 #[error("threshold {threshold} > party_count {party_count}")]
44 ThresholdExceedsCount {
45 threshold: u32,
47 party_count: u32,
49 },
50 #[error("threshold is 0")]
52 ZeroThreshold,
53 #[error("public key has wrong length: {actual} (expected 33 or 65)")]
55 PublicKeyLength { actual: usize },
56 #[error("public key hex is invalid: {0}")]
58 PublicKeyHex(String),
59 #[error("scalar hex is invalid: {0}")]
61 ScalarHex(String),
62}
63
64pub fn check_share(share: &NormalizedShare) -> IntegrityResult {
67 let mut issues = Vec::new();
68
69 check_threshold(&mut issues, share);
70 check_party_idx(&mut issues, share);
71 check_scalar(&mut issues, share);
72 check_public_key(&mut issues, share);
73
74 if issues.is_empty() {
75 IntegrityResult::Valid
76 } else {
77 IntegrityResult::Invalid(issues)
78 }
79}
80
81pub fn is_valid(share: &NormalizedShare) -> bool {
83 matches!(check_share(share), IntegrityResult::Valid)
84}
85
86fn check_threshold(issues: &mut Vec<IntegrityIssue>, share: &NormalizedShare) {
87 if share.threshold == 0 {
88 issues.push(IntegrityIssue::ZeroThreshold);
89 }
90 if share.threshold > share.party_count {
91 issues.push(IntegrityIssue::ThresholdExceedsCount {
92 threshold: share.threshold,
93 party_count: share.party_count,
94 });
95 }
96}
97
98fn check_party_idx(issues: &mut Vec<IntegrityIssue>, share: &NormalizedShare) {
99 if share.party_idx == 0 {
100 issues.push(IntegrityIssue::PartyIdxZero);
101 }
102 if share.party_idx > share.party_count {
103 issues.push(IntegrityIssue::PartyIdxExceedsCount {
104 party_idx: share.party_idx,
105 party_count: share.party_count,
106 });
107 }
108}
109
110fn check_scalar(issues: &mut Vec<IntegrityIssue>, share: &NormalizedShare) {
111 let bytes = match share.scalar_bytes() {
112 Ok(b) => b,
113 Err(e) => {
114 issues.push(IntegrityIssue::ScalarHex(e.to_string()));
115 return;
116 }
117 };
118 if bytes.len() != 32 {
119 issues.push(IntegrityIssue::ScalarLength {
120 actual: bytes.len(),
121 });
122 return;
123 }
124 if bytes.iter().all(|&b| b == 0) {
125 issues.push(IntegrityIssue::ZeroScalar);
126 }
127}
128
129fn check_public_key(issues: &mut Vec<IntegrityIssue>, share: &NormalizedShare) {
130 let bytes = match share.public_key_bytes() {
131 Ok(b) => b,
132 Err(e) => {
133 issues.push(IntegrityIssue::PublicKeyHex(e.to_string()));
134 return;
135 }
136 };
137 let len = bytes.len();
138 if len != 33 && len != 65 {
139 issues.push(IntegrityIssue::PublicKeyLength { actual: len });
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146
147 fn make_valid_share() -> NormalizedShare {
148 NormalizedShare::new("CMP20", "quorum-1", 2, 3, 5, &[0x42; 32], &[0x04; 65]).unwrap()
149 }
150
151 #[test]
152 fn valid_share_passes() {
153 let share = make_valid_share();
154 assert!(matches!(check_share(&share), IntegrityResult::Valid));
155 assert!(is_valid(&share));
156 }
157
158 #[test]
159 fn zero_scalar_rejected() {
160 let mut share = make_valid_share();
161 share.scalar_hex = hex::encode([0u8; 32]);
162 let result = check_share(&share);
163 assert!(
164 matches!(result, IntegrityResult::Invalid(issues) if issues.contains(&IntegrityIssue::ZeroScalar))
165 );
166 }
167
168 #[test]
169 fn zero_threshold_rejected() {
170 let mut share = make_valid_share();
171 share.threshold = 0;
172 let result = check_share(&share);
173 assert!(
174 matches!(result, IntegrityResult::Invalid(issues) if issues.contains(&IntegrityIssue::ZeroThreshold))
175 );
176 }
177
178 #[test]
179 fn threshold_exceeds_count_rejected() {
180 let mut share = make_valid_share();
181 share.threshold = 10;
182 share.party_count = 5;
183 let result = check_share(&share);
184 assert!(
185 matches!(result, IntegrityResult::Invalid(issues) if issues.iter().any(|i| matches!(i, IntegrityIssue::ThresholdExceedsCount { .. })))
186 );
187 }
188
189 #[test]
190 fn zero_party_idx_rejected() {
191 let mut share = make_valid_share();
192 share.party_idx = 0;
193 let result = check_share(&share);
194 assert!(
195 matches!(result, IntegrityResult::Invalid(issues) if issues.contains(&IntegrityIssue::PartyIdxZero))
196 );
197 }
198
199 #[test]
200 fn party_idx_exceeds_count_rejected() {
201 let mut share = make_valid_share();
202 share.party_idx = 10;
203 share.party_count = 5;
204 let result = check_share(&share);
205 assert!(
206 matches!(result, IntegrityResult::Invalid(issues) if issues.iter().any(|i| matches!(i, IntegrityIssue::PartyIdxExceedsCount { .. })))
207 );
208 }
209
210 #[test]
211 fn compressed_pubkey_accepted() {
212 let mut share = make_valid_share();
213 share.public_key_hex = hex::encode([0x02; 33]);
214 assert!(is_valid(&share));
215 }
216
217 #[test]
218 fn wrong_pubkey_length_rejected() {
219 let mut share = make_valid_share();
220 share.public_key_hex = hex::encode([0x04; 10]);
221 let result = check_share(&share);
222 assert!(
223 matches!(result, IntegrityResult::Invalid(issues) if issues.iter().any(|i| matches!(i, IntegrityIssue::PublicKeyLength { .. })))
224 );
225 }
226
227 #[test]
228 fn bad_scalar_hex_rejected() {
229 let mut share = make_valid_share();
230 share.scalar_hex = "not-hex!!".into();
231 let result = check_share(&share);
232 assert!(matches!(result, IntegrityResult::Invalid(_)));
233 }
234
235 #[test]
236 fn multiple_issues_reported() {
237 let mut share = make_valid_share();
238 share.party_idx = 0;
239 share.threshold = 0;
240 share.scalar_hex = "00".repeat(32);
241 let result = check_share(&share);
242 match result {
243 IntegrityResult::Invalid(issues) => assert!(issues.len() >= 3),
244 _ => panic!("expected Invalid"),
245 }
246 }
247
248 #[test]
249 fn is_valid_shorthand_works() {
250 assert!(is_valid(&make_valid_share()));
251 }
252}