|
6 | 6 | //! The schema is frozen at v1. All fields are mandatory for forward compatibility. |
7 | 7 | //! Nodes that receive a certificate with an unknown version MUST reject it. |
8 | 8 |
|
| 9 | +use std::collections::HashSet; |
| 10 | + |
9 | 11 | use chrono::{DateTime, Utc}; |
10 | 12 | use serde::{Deserialize, Serialize}; |
11 | 13 | use uuid::Uuid; |
@@ -135,10 +137,14 @@ impl RefUpdateCert { |
135 | 137 |
|
136 | 138 | /// Check if this certificate satisfies a threshold of valid signatures |
137 | 139 | /// from the provided set of authorized maintainer DIDs. |
| 140 | + /// |
| 141 | + /// Counts distinct signer DIDs, not signature entries: a repeated |
| 142 | + /// signature from the same maintainer counts once. |
138 | 143 | pub fn satisfies_threshold(&self, maintainers: &[Did], threshold: usize) -> Result<bool> { |
139 | 144 | let valid = self.verify_all()?; |
140 | | - let count = valid.iter().filter(|d| maintainers.contains(d)).count(); |
141 | | - Ok(count >= threshold) |
| 145 | + let distinct_signers: HashSet<&Did> = |
| 146 | + valid.iter().filter(|d| maintainers.contains(d)).collect(); |
| 147 | + Ok(distinct_signers.len() >= threshold) |
142 | 148 | } |
143 | 149 |
|
144 | 150 | /// Validate the certificate structure (not signatures). |
@@ -344,6 +350,33 @@ mod tests { |
344 | 350 | assert!(!cert.satisfies_threshold(&maintainers, 1).unwrap()); |
345 | 351 | } |
346 | 352 |
|
| 353 | + #[test] |
| 354 | + fn satisfies_threshold_rejects_duplicated_signature() { |
| 355 | + let kp1 = Keypair::generate(); |
| 356 | + let kp2 = Keypair::generate(); |
| 357 | + let kp3 = Keypair::generate(); |
| 358 | + let repo_did = kp1.did(); |
| 359 | + |
| 360 | + let mut cert = RefUpdateCert::new( |
| 361 | + repo_did, |
| 362 | + "refs/heads/main".to_string(), |
| 363 | + dummy_hash('0'), |
| 364 | + dummy_hash('a'), |
| 365 | + 1, |
| 366 | + &kp1, |
| 367 | + ) |
| 368 | + .unwrap(); |
| 369 | + // Copy-paste the only real signature onto the certificate a second |
| 370 | + // time. Same signer, same valid signature, still one real signer. |
| 371 | + let dup = cert.signatures[0].clone(); |
| 372 | + cert.signatures.push(dup); |
| 373 | + |
| 374 | + let maintainers = vec![kp1.did(), kp2.did(), kp3.did()]; |
| 375 | + // Two signature entries but a single distinct signer must not |
| 376 | + // satisfy a 2-of-3 threshold. |
| 377 | + assert!(!cert.satisfies_threshold(&maintainers, 2).unwrap()); |
| 378 | + } |
| 379 | + |
347 | 380 | #[test] |
348 | 381 | fn threshold_zero_is_always_satisfied() { |
349 | 382 | let kp = Keypair::generate(); |
|
0 commit comments