confium_signatif/
multilog.rs1use serde::{Deserialize, Serialize};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
15pub struct MultiLogPolicy {
16 pub m: usize,
18 pub k: usize,
20}
21
22impl MultiLogPolicy {
23 pub fn validate(&self) -> crate::error::SignatifResult<()> {
29 if self.m == 0 || self.m > self.k {
30 return Err(crate::error::SignatifError::Encoding(format!(
31 "invalid multi-log policy {} of {}",
32 self.m, self.k
33 )));
34 }
35 Ok(())
36 }
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct LogInclusion {
42 pub log: String,
44 pub included: bool,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct MultiLogAttestation {
51 pub inclusions: Vec<LogInclusion>,
53}
54
55impl MultiLogAttestation {
56 pub fn satisfies(&self, policy: &MultiLogPolicy) -> crate::error::SignatifResult<bool> {
62 policy.validate()?;
63 let included = self.inclusions.iter().filter(|i| i.included).count();
64 Ok(included >= policy.m)
65 }
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct WitnessCosignature {
71 pub witness: String,
73 pub tree_head_bytes: Vec<u8>,
75 pub signature: Vec<u8>,
77 pub public_key: Vec<u8>,
79}
80
81#[derive(Debug, Clone, Copy)]
84pub struct GossipQuorum {
85 pub min_sources: usize,
87}
88
89impl GossipQuorum {
90 pub fn check(
96 &self,
97 cosignatures: &[WitnessCosignature],
98 verifier: &dyn crate::graph::SignatureVerifier,
99 ) -> crate::error::SignatifResult<bool> {
100 use std::collections::BTreeMap;
101 let mut by_head: BTreeMap<Vec<u8>, Vec<&WitnessCosignature>> = BTreeMap::new();
103 for c in cosignatures {
104 by_head
105 .entry(c.tree_head_bytes.clone())
106 .or_default()
107 .push(c);
108 }
109 for (_, witnesses) in by_head {
110 let mut distinct = std::collections::BTreeSet::new();
111 let mut all_valid = true;
112 for w in &witnesses {
113 if !verifier.verify(&w.public_key, &w.tree_head_bytes, &w.signature) {
114 all_valid = false;
115 break;
116 }
117 distinct.insert(w.witness.clone());
118 }
119 if all_valid && distinct.len() >= self.min_sources {
120 return Ok(true);
121 }
122 }
123 Ok(false)
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130 use ed25519_dalek::{Signer, SigningKey};
131
132 fn generate_key() -> ed25519_dalek::SigningKey {
133 use rand_core::RngCore;
134 let mut seed = [0u8; 32];
135 rand_core::OsRng.fill_bytes(&mut seed);
136 ed25519_dalek::SigningKey::from_bytes(&seed)
137 }
138
139 struct Ed25519Verifier;
140
141 impl crate::graph::SignatureVerifier for Ed25519Verifier {
142 fn verify(&self, pk: &[u8], msg: &[u8], sig: &[u8]) -> bool {
143 use ed25519_dalek::Signature;
144 use ed25519_dalek::Verifier;
145 let Ok(vk) = ed25519_dalek::VerifyingKey::from_bytes(pk.try_into().unwrap()) else {
146 return false;
147 };
148 let Ok(signature) = Signature::from_slice(sig) else {
149 return false;
150 };
151 vk.verify(msg, &signature).is_ok()
152 }
153 }
154
155 #[test]
156 fn multilog_quorum() {
157 let policy = MultiLogPolicy { m: 2, k: 3 };
158 let att = MultiLogAttestation {
159 inclusions: vec![
160 LogInclusion {
161 log: "a".into(),
162 included: true,
163 },
164 LogInclusion {
165 log: "b".into(),
166 included: true,
167 },
168 LogInclusion {
169 log: "c".into(),
170 included: false,
171 },
172 ],
173 };
174 assert!(att.satisfies(&policy).unwrap());
175 let weak = MultiLogAttestation {
176 inclusions: vec![
177 LogInclusion {
178 log: "a".into(),
179 included: true,
180 },
181 LogInclusion {
182 log: "b".into(),
183 included: false,
184 },
185 LogInclusion {
186 log: "c".into(),
187 included: false,
188 },
189 ],
190 };
191 assert!(!weak.satisfies(&policy).unwrap());
192 assert!(policy.with_m_zero().validate().is_err());
193 }
194
195 impl MultiLogPolicy {
196 fn with_m_zero(&self) -> MultiLogPolicy {
197 MultiLogPolicy { m: 0, k: self.k }
198 }
199 }
200
201 #[test]
202 fn gossip_quorum_needs_agreement() {
203 let head = b"tree-head-v1".to_vec();
204 let witnesses: Vec<SigningKey> = (0..3).map(|_| generate_key()).collect();
205 let cosigns: Vec<WitnessCosignature> = witnesses
206 .iter()
207 .enumerate()
208 .map(|(i, sk)| WitnessCosignature {
209 witness: format!("w{i}"),
210 tree_head_bytes: head.clone(),
211 signature: sk.sign(&head).to_bytes().to_vec(),
212 public_key: sk.verifying_key().as_bytes().to_vec(),
213 })
214 .collect();
215 let q = GossipQuorum { min_sources: 3 };
216 assert!(q.check(&cosigns, &Ed25519Verifier).unwrap());
217
218 let mut split = cosigns.clone();
220 split[2].tree_head_bytes = b"other-head".to_vec();
221 assert!(!q.check(&split, &Ed25519Verifier).unwrap());
222
223 let mut forged = cosigns.clone();
225 forged[1].signature[0] ^= 1;
226 assert!(!q.check(&forged, &Ed25519Verifier).unwrap());
227 }
228}