1use std::collections::BTreeMap;
22
23use serde::{Deserialize, Serialize};
24use sha2::{Digest, Sha256};
25
26use crate::error::{SignatifError, SignatifResult};
27use crate::graph::{Quorum, SignatureVerifier};
28
29#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
31pub struct MemberOrg {
32 pub id: String,
34 pub public_key: Vec<u8>,
37 #[serde(default, skip_serializing_if = "Option::is_none")]
40 pub inner_quorum: Option<Quorum>,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct FederatedTrustAuthority {
46 pub id: String,
48 pub members: Vec<MemberOrg>,
50 pub org_quorum: Quorum,
52 pub aggregate_key: Vec<u8>,
54 pub hierarchy_spanning: bool,
57}
58
59impl FederatedTrustAuthority {
60 pub fn validate(&self) -> SignatifResult<()> {
67 if self.org_quorum.t == 0 || self.org_quorum.t as usize > self.members.len() {
68 return Err(SignatifError::Encoding(format!(
69 "FTA {}: org quorum {} of {} inconsistent with {} members",
70 self.id,
71 self.org_quorum.t,
72 self.org_quorum.n,
73 self.members.len()
74 )));
75 }
76 if self.org_quorum.n as usize != self.members.len() {
77 return Err(SignatifError::Encoding(format!(
78 "FTA {}: K ({}) must equal member count ({})",
79 self.id,
80 self.org_quorum.n,
81 self.members.len()
82 )));
83 }
84 let mut seen = std::collections::BTreeSet::new();
85 for m in &self.members {
86 if !seen.insert(m.id.clone()) {
87 return Err(SignatifError::Encoding(format!(
88 "FTA {}: duplicate member {}",
89 self.id, m.id
90 )));
91 }
92 if let Some(q) = m.inner_quorum {
93 Quorum::new(q.t, q.n)?;
94 }
95 }
96 Ok(())
97 }
98
99 pub fn requires_inner_quorum(&self, org: &str) -> Option<Quorum> {
102 self.members
103 .iter()
104 .find(|m| m.id == org)
105 .and_then(|m| m.inner_quorum)
106 }
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct ProofOfPossession {
115 pub member: String,
117 pub public_key: Vec<u8>,
119 pub signature: Vec<u8>,
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct DkgSession {
126 pub authority_id: String,
128 pub session_nonce: [u8; 32],
130 pub ceremony_type: String,
132}
133
134impl DkgSession {
135 pub fn pop_bytes(&self, member: &str, public_key: &[u8]) -> Vec<u8> {
137 let mut bytes = Vec::new();
138 bytes.extend_from_slice(self.authority_id.as_bytes());
139 bytes.push(0);
140 bytes.extend_from_slice(&self.session_nonce);
141 bytes.push(0);
142 bytes.extend_from_slice(member.as_bytes());
143 bytes.push(0);
144 bytes.extend_from_slice(public_key);
145 bytes
146 }
147
148 fn verify_pop(&self, pop: &ProofOfPossession, verifier: &dyn SignatureVerifier) -> bool {
150 verifier.verify(
151 &pop.public_key,
152 &self.pop_bytes(&pop.member, &pop.public_key),
153 &pop.signature,
154 )
155 }
156
157 pub fn verify_contributions(
165 &self,
166 pops: &[ProofOfPossession],
167 quorum: Quorum,
168 verifier: &dyn SignatureVerifier,
169 ) -> SignatifResult<()> {
170 let mut distinct = std::collections::BTreeSet::new();
171 for pop in pops {
172 if !self.verify_pop(pop, verifier) {
173 return Err(SignatifError::Ceremony(format!(
174 "rogue-key defense: member {} failed proof of possession",
175 pop.member
176 )));
177 }
178 distinct.insert(pop.member.clone());
179 }
180 if distinct.len() < quorum.t as usize {
181 return Err(SignatifError::Ceremony(format!(
182 "quorum not met: {} of {} required, {} contributed",
183 quorum.t,
184 quorum.n,
185 distinct.len()
186 )));
187 }
188 Ok(())
189 }
190}
191
192#[derive(Debug, Clone, Serialize, Deserialize)]
194pub struct NonceCommitment {
195 pub member: String,
197 pub commitment: [u8; 32],
199}
200
201#[derive(Debug, Clone, Serialize, Deserialize)]
203pub struct NonceReveal {
204 pub member: String,
206 pub nonce: [u8; 32],
208}
209
210impl NonceCommitment {
211 pub fn commit(member: impl Into<String>, nonce: &[u8; 32]) -> Self {
213 Self {
214 member: member.into(),
215 commitment: Sha256::digest(nonce).into(),
216 }
217 }
218
219 pub fn verify(&self, reveal: &NonceReveal) -> bool {
221 self.member == reveal.member
222 && self.commitment == <[u8; 32]>::from(Sha256::digest(reveal.nonce))
223 }
224}
225
226#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
228pub struct AbortAttribution {
229 pub faulty_member: String,
231 pub reason: AbortReason,
233}
234
235#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
237#[serde(rename_all = "snake_case")]
238pub enum AbortReason {
239 BadProofOfPossession,
241 NonceCommitmentMismatch,
243 Timeout,
245 MalformedContribution,
247}
248
249pub fn attribute_dkg_failure(
257 session: &DkgSession,
258 expected: &[MemberOrg],
259 pops: &[ProofOfPossession],
260 commitments: &[NonceCommitment],
261 reveals: &[NonceReveal],
262 verifier: &dyn SignatureVerifier,
263) -> Result<(), AbortAttribution> {
264 let by_member: BTreeMap<&str, &ProofOfPossession> =
265 pops.iter().map(|p| (p.member.as_str(), p)).collect();
266
267 for org in expected {
268 let Some(pop) = by_member.get(org.id.as_str()) else {
269 return Err(AbortAttribution {
270 faulty_member: org.id.clone(),
271 reason: AbortReason::Timeout,
272 });
273 };
274 if !session.verify_pop(pop, verifier) {
275 return Err(AbortAttribution {
276 faulty_member: org.id.clone(),
277 reason: AbortReason::BadProofOfPossession,
278 });
279 }
280 }
281
282 let commits: BTreeMap<&str, &NonceCommitment> =
283 commitments.iter().map(|c| (c.member.as_str(), c)).collect();
284 let revealed: BTreeMap<&str, &NonceReveal> =
285 reveals.iter().map(|r| (r.member.as_str(), r)).collect();
286 for (member, commit) in &commits {
287 match revealed.get(*member) {
288 None => {
289 return Err(AbortAttribution {
290 faulty_member: (*member).to_string(),
291 reason: AbortReason::Timeout,
292 });
293 }
294 Some(reveal) => {
295 if !commit.verify(reveal) {
296 return Err(AbortAttribution {
297 faulty_member: (*member).to_string(),
298 reason: AbortReason::NonceCommitmentMismatch,
299 });
300 }
301 }
302 }
303 }
304 Ok(())
305}
306
307#[derive(Debug, Clone, Serialize, Deserialize)]
310pub struct MembershipChange {
311 pub kind: MembershipChangeKind,
313 pub aggregate_key_before: Vec<u8>,
315 pub aggregate_key_after: Vec<u8>,
317}
318
319#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
321#[serde(tag = "kind", rename_all = "snake_case")]
322pub enum MembershipChangeKind {
323 Join {
325 org: MemberOrg,
327 },
328 Leave {
330 org_id: String,
332 },
333 Dissolve,
336}
337
338impl MembershipChange {
339 pub fn validate(&self) -> SignatifResult<()> {
347 match &self.kind {
348 MembershipChangeKind::Dissolve => Ok(()),
349 _ => {
350 if self.aggregate_key_before != self.aggregate_key_after {
351 return Err(SignatifError::Encoding(
352 "membership change altered the aggregate key — join/leave must re-share while preserving it"
353 .into(),
354 ));
355 }
356 Ok(())
357 }
358 }
359 }
360}
361
362#[cfg(test)]
363mod tests {
364 use super::*;
365 use ed25519_dalek::Signer;
366 use rand_core::RngCore;
367
368 fn generate_key() -> ed25519_dalek::SigningKey {
369 let mut seed = [0u8; 32];
370 rand_core::OsRng.fill_bytes(&mut seed);
371 ed25519_dalek::SigningKey::from_bytes(&seed)
372 }
373
374 struct Ed25519Verifier;
375
376 impl SignatureVerifier for Ed25519Verifier {
377 fn verify(&self, pk: &[u8], msg: &[u8], sig: &[u8]) -> bool {
378 use ed25519_dalek::Signature;
379 use ed25519_dalek::Verifier;
380 let Ok(vk) = ed25519_dalek::VerifyingKey::from_bytes(pk.try_into().unwrap()) else {
381 return false;
382 };
383 let Ok(signature) = Signature::from_slice(sig) else {
384 return false;
385 };
386 vk.verify(msg, &signature).is_ok()
387 }
388 }
389
390 fn org(id: &str, sk: &ed25519_dalek::SigningKey, inner: Option<Quorum>) -> MemberOrg {
391 MemberOrg {
392 id: id.into(),
393 public_key: sk.verifying_key().as_bytes().to_vec(),
394 inner_quorum: inner,
395 }
396 }
397
398 fn session() -> DkgSession {
399 let mut nonce = [0u8; 32];
400 rand_core::OsRng.fill_bytes(&mut nonce);
401 DkgSession {
402 authority_id: "fta-pharma".into(),
403 session_nonce: nonce,
404 ceremony_type: "dkg".into(),
405 }
406 }
407
408 fn pops(
409 session: &DkgSession,
410 keys: &[(&str, &ed25519_dalek::SigningKey)],
411 ) -> Vec<ProofOfPossession> {
412 keys.iter()
413 .map(|(id, sk)| {
414 let pk = sk.verifying_key().as_bytes().to_vec();
415 let signature = sk.sign(&session.pop_bytes(id, &pk)).to_bytes().to_vec();
416 ProofOfPossession {
417 member: (*id).into(),
418 public_key: pk,
419 signature,
420 }
421 })
422 .collect()
423 }
424
425 #[test]
426 fn fta_structure_validation() {
427 let (a, b, c) = (generate_key(), generate_key(), generate_key());
428 let fta = FederatedTrustAuthority {
429 id: "fta".into(),
430 members: vec![
431 org("org-a", &a, Some(Quorum { t: 2, n: 3 })),
432 org("org-b", &b, None),
433 org("org-c", &c, None),
434 ],
435 org_quorum: Quorum { t: 2, n: 3 },
436 aggregate_key: vec![1],
437 hierarchy_spanning: true,
438 };
439 assert!(fta.validate().is_ok());
440 assert_eq!(
441 fta.requires_inner_quorum("org-a"),
442 Some(Quorum { t: 2, n: 3 })
443 );
444 assert_eq!(fta.requires_inner_quorum("org-b"), None);
445
446 let mut bad = fta.clone();
447 bad.members.push(bad.members[0].clone());
448 assert!(bad.validate().is_err());
449 let mut badq = fta;
450 badq.org_quorum = Quorum { t: 5, n: 3 };
451 assert!(badq.validate().is_err());
452 }
453
454 #[test]
455 fn rogue_key_defense_rejects_forged_pop() {
456 let session = session();
457 let keys = [("org-a", generate_key()), ("org-b", generate_key())];
458 let refs: Vec<(&str, &ed25519_dalek::SigningKey)> =
459 keys.iter().map(|(id, sk)| (*id, sk)).collect();
460 let mut good = pops(&session, &refs);
461 let mut forged = good[1].clone();
463 forged.member = "org-a".into();
464 good[1] = forged;
465
466 let err = session
467 .verify_contributions(&good, Quorum { t: 2, n: 2 }, &Ed25519Verifier)
468 .unwrap_err();
469 assert!(err.to_string().contains("rogue-key"), "got {err}");
470 }
471
472 #[test]
473 fn dkg_quorum_enforced() {
474 let session = session();
475 let a = generate_key();
476 let keys = [("org-a", &a)];
477 let good = pops(&session, &keys);
478 assert!(
480 session
481 .verify_contributions(&good, Quorum { t: 2, n: 2 }, &Ed25519Verifier)
482 .is_err()
483 );
484 }
485
486 #[test]
487 fn nonce_commitment_round_trip() {
488 let mut nonce = [0u8; 32];
489 rand_core::OsRng.fill_bytes(&mut nonce);
490 let c = NonceCommitment::commit("org-a", &nonce);
491 assert!(c.verify(&NonceReveal {
492 member: "org-a".into(),
493 nonce,
494 }));
495 let mut wrong = nonce;
496 wrong[0] ^= 1;
497 assert!(!c.verify(&NonceReveal {
498 member: "org-a".into(),
499 nonce: wrong,
500 }));
501 }
502
503 #[test]
504 fn identifiable_abort_names_the_culprit() {
505 let session = session();
506 let a = generate_key();
507 let b = generate_key();
508 let expected = vec![org("org-a", &a, None), org("org-b", &b, None)];
509 let keys = [("org-a", &a)];
510 let only_a = pops(&session, &keys);
511 let err = attribute_dkg_failure(&session, &expected, &only_a, &[], &[], &Ed25519Verifier)
512 .unwrap_err();
513 assert_eq!(err.faulty_member, "org-b");
514 assert_eq!(err.reason, AbortReason::Timeout);
515
516 let both = pops(&session, &[("org-a", &a), ("org-b", &b)]);
520 let mut nonce = [7u8; 32];
521 let commitment = NonceCommitment::commit("org-a", &nonce);
522 nonce[0] ^= 1;
523 let reveal = NonceReveal {
524 member: "org-a".into(),
525 nonce,
526 };
527 let err = attribute_dkg_failure(
528 &session,
529 &expected,
530 &both,
531 &[commitment],
532 &[reveal],
533 &Ed25519Verifier,
534 )
535 .unwrap_err();
536 assert_eq!(err.reason, AbortReason::NonceCommitmentMismatch);
537 }
538
539 #[test]
540 fn membership_changes_preserve_aggregate() {
541 let key = vec![9u8; 32];
542 let join = MembershipChange {
543 kind: MembershipChangeKind::Join {
544 org: org("org-d", &generate_key(), None),
545 },
546 aggregate_key_before: key.clone(),
547 aggregate_key_after: key.clone(),
548 };
549 assert!(join.validate().is_ok());
550
551 let leave = MembershipChange {
552 kind: MembershipChangeKind::Leave {
553 org_id: "org-a".into(),
554 },
555 aggregate_key_before: key.clone(),
556 aggregate_key_after: vec![8u8; 32],
557 };
558 assert!(leave.validate().is_err());
559
560 let dissolve = MembershipChange {
561 kind: MembershipChangeKind::Dissolve,
562 aggregate_key_before: key,
563 aggregate_key_after: vec![],
564 };
565 assert!(dissolve.validate().is_ok());
566 }
567}