Skip to main content

confium_tc_keys/
integrity.rs

1//! Share integrity verification — validates shares before use.
2//!
3//! Corrupted shares cause signing failures that are hard to diagnose.
4//! This module validates share structure, scalar range, party index
5//! bounds, and public key format before the share enters the signing
6//! pipeline.
7
8use confium_tc_core::share_adapter::NormalizedShare;
9
10/// Result of integrity checking.
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum IntegrityResult {
13    /// Share passed all checks.
14    Valid,
15    /// Share has one or more problems.
16    Invalid(Vec<IntegrityIssue>),
17}
18
19/// A specific integrity issue found in a share.
20#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
21pub enum IntegrityIssue {
22    /// Scalar is zero (invalid for any threshold scheme).
23    #[error("scalar is zero")]
24    ZeroScalar,
25    /// Scalar bytes are wrong length.
26    #[error("scalar has wrong length: {actual} (expected 32)")]
27    ScalarLength { actual: usize },
28    /// Scalar is >= curve order.
29    #[error("scalar >= curve order")]
30    ScalarOutOfRange,
31    /// Party index is zero (must be 1-based).
32    #[error("party_idx is 0 (must be >= 1)")]
33    PartyIdxZero,
34    /// Party index exceeds party count.
35    #[error("party_idx {party_idx} > party_count {party_count}")]
36    PartyIdxExceedsCount {
37        /// The party index.
38        party_idx: u32,
39        /// The party count.
40        party_count: u32,
41    },
42    /// Threshold exceeds party count.
43    #[error("threshold {threshold} > party_count {party_count}")]
44    ThresholdExceedsCount {
45        /// The threshold.
46        threshold: u32,
47        /// The party count.
48        party_count: u32,
49    },
50    /// Threshold is zero.
51    #[error("threshold is 0")]
52    ZeroThreshold,
53    /// Public key is wrong length.
54    #[error("public key has wrong length: {actual} (expected 33 or 65)")]
55    PublicKeyLength { actual: usize },
56    /// Public key hex is invalid.
57    #[error("public key hex is invalid: {0}")]
58    PublicKeyHex(String),
59    /// Scalar hex is invalid.
60    #[error("scalar hex is invalid: {0}")]
61    ScalarHex(String),
62}
63
64/// Check the integrity of a normalized share. Returns `Valid` if all
65/// checks pass, or `Invalid` with a list of issues.
66pub 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
81/// Quick boolean check — returns true if the share is valid.
82pub 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}