Skip to content

Commit 50d3cbb

Browse files
authored
Merge pull request #326 from euxaristia/fix/cert-threshold-duplicate-signatures
fix(core): Count distinct signer DIDs in certificate threshold check
2 parents 96d8123 + 3993fd1 commit 50d3cbb

1 file changed

Lines changed: 35 additions & 2 deletions

File tree

‎crates/gitlawb-core/src/cert.rs‎

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
//! The schema is frozen at v1. All fields are mandatory for forward compatibility.
77
//! Nodes that receive a certificate with an unknown version MUST reject it.
88
9+
use std::collections::HashSet;
10+
911
use chrono::{DateTime, Utc};
1012
use serde::{Deserialize, Serialize};
1113
use uuid::Uuid;
@@ -135,10 +137,14 @@ impl RefUpdateCert {
135137

136138
/// Check if this certificate satisfies a threshold of valid signatures
137139
/// 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.
138143
pub fn satisfies_threshold(&self, maintainers: &[Did], threshold: usize) -> Result<bool> {
139144
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)
142148
}
143149

144150
/// Validate the certificate structure (not signatures).
@@ -344,6 +350,33 @@ mod tests {
344350
assert!(!cert.satisfies_threshold(&maintainers, 1).unwrap());
345351
}
346352

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+
347380
#[test]
348381
fn threshold_zero_is_always_satisfied() {
349382
let kp = Keypair::generate();

0 commit comments

Comments
 (0)