1use serde::{Deserialize, Serialize};
14
15use crate::keys::{self, UserKeypair};
16
17#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
18pub enum MembershipAction {
19 Add,
20 Remove,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
24pub enum MemberRole {
25 Owner,
26 Member,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct MembershipEntry {
32 pub action: MembershipAction,
33 pub user_pubkey: String,
34 pub role: MemberRole,
35 pub timestamp: String,
36 pub author_pubkey: String,
37 pub signature: String,
38}
39
40#[derive(Debug, thiserror::Error)]
41pub enum MembershipError {
42 #[error("first entry must be a self-signed owner Add")]
43 InvalidFirstEntry,
44 #[error("entry at index {0} has an invalid signature")]
45 InvalidSignature(usize),
46 #[error("entry at index {0}: author is not an owner at that point in the chain")]
47 NotAnOwner(usize),
48 #[error("chain is empty")]
49 EmptyChain,
50}
51
52pub fn canonical_bytes(entry: &MembershipEntry) -> Vec<u8> {
54 let canonical = serde_json::json!({
57 "action": entry.action,
58 "author_pubkey": entry.author_pubkey,
59 "role": entry.role,
60 "timestamp": entry.timestamp,
61 "user_pubkey": entry.user_pubkey,
62 });
63 serde_json::to_vec(&canonical).expect("canonical serialization cannot fail")
64}
65
66pub fn sign_membership_entry(entry: &mut MembershipEntry, keypair: &UserKeypair) {
70 entry.author_pubkey = hex::encode(keypair.public_key);
71 let bytes = canonical_bytes(entry);
72 let sig = keypair.sign(&bytes);
73 entry.signature = hex::encode(sig);
74}
75
76pub fn verify_membership_entry(entry: &MembershipEntry) -> bool {
78 let Ok(pk_bytes) = hex::decode(&entry.author_pubkey) else {
79 return false;
80 };
81 let Ok(sig_bytes) = hex::decode(&entry.signature) else {
82 return false;
83 };
84
85 let Ok(pk): Result<[u8; keys::SIGN_PUBLICKEYBYTES], _> = pk_bytes.try_into() else {
86 return false;
87 };
88 let Ok(sig): Result<[u8; keys::SIGN_BYTES], _> = sig_bytes.try_into() else {
89 return false;
90 };
91
92 let bytes = canonical_bytes(entry);
93 keys::verify_signature(&sig, &bytes, &pk)
94}
95
96#[derive(Debug, Clone, Default)]
100pub struct MembershipChain {
101 entries: Vec<MembershipEntry>,
102}
103
104impl MembershipChain {
105 pub fn new() -> Self {
107 Self::default()
108 }
109
110 pub fn from_entries(mut entries: Vec<MembershipEntry>) -> Result<Self, MembershipError> {
113 entries.sort_by(|a, b| a.timestamp.cmp(&b.timestamp));
114 let chain = Self { entries };
115 chain.validate()?;
116 Ok(chain)
117 }
118
119 pub fn entries(&self) -> &[MembershipEntry] {
121 &self.entries
122 }
123
124 pub fn validate(&self) -> Result<(), MembershipError> {
131 if self.entries.is_empty() {
132 return Err(MembershipError::EmptyChain);
133 }
134
135 let first = &self.entries[0];
136 if first.action != MembershipAction::Add
137 || first.role != MemberRole::Owner
138 || first.author_pubkey != first.user_pubkey
139 {
140 return Err(MembershipError::InvalidFirstEntry);
141 }
142
143 if !verify_membership_entry(first) {
144 return Err(MembershipError::InvalidSignature(0));
145 }
146
147 let mut active: Vec<(String, MemberRole)> = vec![];
149 active.push((first.user_pubkey.clone(), first.role.clone()));
150
151 for (i, entry) in self.entries.iter().enumerate().skip(1) {
152 if !verify_membership_entry(entry) {
153 return Err(MembershipError::InvalidSignature(i));
154 }
155
156 let is_owner = active
158 .iter()
159 .any(|(pk, role)| pk == &entry.author_pubkey && *role == MemberRole::Owner);
160
161 if !is_owner {
162 return Err(MembershipError::NotAnOwner(i));
163 }
164
165 match entry.action {
166 MembershipAction::Add => {
167 active.retain(|(pk, _)| pk != &entry.user_pubkey);
169 active.push((entry.user_pubkey.clone(), entry.role.clone()));
170 }
171 MembershipAction::Remove => {
172 active.retain(|(pk, _)| pk != &entry.user_pubkey);
173 }
174 }
175 }
176
177 Ok(())
178 }
179
180 pub fn is_member_at(&self, pubkey: &str, timestamp: &str) -> bool {
184 let mut active: Vec<String> = Vec::new();
185
186 for entry in &self.entries {
187 if entry.timestamp.as_str() > timestamp {
188 break;
189 }
190
191 match entry.action {
192 MembershipAction::Add => {
193 if !active.contains(&entry.user_pubkey) {
194 active.push(entry.user_pubkey.clone());
195 }
196 }
197 MembershipAction::Remove => {
198 active.retain(|pk| pk != &entry.user_pubkey);
199 }
200 }
201 }
202
203 active.contains(&pubkey.to_string())
204 }
205
206 pub fn current_members(&self) -> Vec<(String, MemberRole)> {
208 let mut active: Vec<(String, MemberRole)> = Vec::new();
209
210 for entry in &self.entries {
211 match entry.action {
212 MembershipAction::Add => {
213 active.retain(|(pk, _)| pk != &entry.user_pubkey);
214 active.push((entry.user_pubkey.clone(), entry.role.clone()));
215 }
216 MembershipAction::Remove => {
217 active.retain(|(pk, _)| pk != &entry.user_pubkey);
218 }
219 }
220 }
221
222 active
223 }
224
225 pub fn add_entry(&mut self, entry: MembershipEntry) -> Result<(), MembershipError> {
227 if self.entries.is_empty() {
228 if entry.action != MembershipAction::Add
230 || entry.role != MemberRole::Owner
231 || entry.author_pubkey != entry.user_pubkey
232 {
233 return Err(MembershipError::InvalidFirstEntry);
234 }
235
236 if !verify_membership_entry(&entry) {
237 return Err(MembershipError::InvalidSignature(0));
238 }
239
240 self.entries.push(entry);
241 return Ok(());
242 }
243
244 if !verify_membership_entry(&entry) {
245 return Err(MembershipError::InvalidSignature(self.entries.len()));
246 }
247
248 let members = self.current_members();
250 let is_owner = members
251 .iter()
252 .any(|(pk, role)| pk == &entry.author_pubkey && *role == MemberRole::Owner);
253
254 if !is_owner {
255 return Err(MembershipError::NotAnOwner(self.entries.len()));
256 }
257
258 self.entries.push(entry);
259 Ok(())
260 }
261}
262
263#[cfg(test)]
264mod tests {
265 use super::*;
266 use crate::keys::UserKeypair;
267
268 fn gen_keypair() -> UserKeypair {
269 UserKeypair::generate()
270 }
271
272 fn pubkey_hex(kp: &UserKeypair) -> String {
273 hex::encode(kp.public_key)
274 }
275
276 fn founder_entry(kp: &UserKeypair, timestamp: &str) -> MembershipEntry {
278 let pk_hex = pubkey_hex(kp);
279 let mut entry = MembershipEntry {
280 action: MembershipAction::Add,
281 user_pubkey: pk_hex.clone(),
282 role: MemberRole::Owner,
283 timestamp: timestamp.to_string(),
284 author_pubkey: pk_hex,
285 signature: String::new(),
286 };
287 sign_membership_entry(&mut entry, kp);
288 entry
289 }
290
291 fn make_entry(
293 author: &UserKeypair,
294 action: MembershipAction,
295 subject: &UserKeypair,
296 role: MemberRole,
297 timestamp: &str,
298 ) -> MembershipEntry {
299 let mut entry = MembershipEntry {
300 action,
301 user_pubkey: pubkey_hex(subject),
302 role,
303 timestamp: timestamp.to_string(),
304 author_pubkey: pubkey_hex(author),
305 signature: String::new(),
306 };
307 sign_membership_entry(&mut entry, author);
308 entry
309 }
310
311 #[test]
312 fn single_owner_chain() {
313 let owner = gen_keypair();
314 let entry = founder_entry(&owner, "0000000001000-0000-dev1");
315
316 let mut chain = MembershipChain::new();
317 chain.add_entry(entry).unwrap();
318 chain.validate().unwrap();
319
320 let members = chain.current_members();
321 assert_eq!(members.len(), 1);
322 assert_eq!(members[0].0, pubkey_hex(&owner));
323 assert_eq!(members[0].1, MemberRole::Owner);
324 }
325
326 #[test]
327 fn add_member_signed_by_owner() {
328 let owner = gen_keypair();
329 let member = gen_keypair();
330
331 let mut chain = MembershipChain::new();
332 chain
333 .add_entry(founder_entry(&owner, "0000000001000-0000-dev1"))
334 .unwrap();
335 chain
336 .add_entry(make_entry(
337 &owner,
338 MembershipAction::Add,
339 &member,
340 MemberRole::Member,
341 "0000000002000-0000-dev1",
342 ))
343 .unwrap();
344
345 chain.validate().unwrap();
346
347 let members = chain.current_members();
348 assert_eq!(members.len(), 2);
349 assert!(members
350 .iter()
351 .any(|(pk, r)| pk == &pubkey_hex(&owner) && *r == MemberRole::Owner));
352 assert!(members
353 .iter()
354 .any(|(pk, r)| pk == &pubkey_hex(&member) && *r == MemberRole::Member));
355 }
356
357 #[test]
358 fn remove_member_signed_by_owner() {
359 let owner = gen_keypair();
360 let member = gen_keypair();
361
362 let mut chain = MembershipChain::new();
363 chain
364 .add_entry(founder_entry(&owner, "0000000001000-0000-dev1"))
365 .unwrap();
366 chain
367 .add_entry(make_entry(
368 &owner,
369 MembershipAction::Add,
370 &member,
371 MemberRole::Member,
372 "0000000002000-0000-dev1",
373 ))
374 .unwrap();
375 chain
376 .add_entry(make_entry(
377 &owner,
378 MembershipAction::Remove,
379 &member,
380 MemberRole::Member,
381 "0000000003000-0000-dev1",
382 ))
383 .unwrap();
384
385 chain.validate().unwrap();
386
387 let members = chain.current_members();
388 assert_eq!(members.len(), 1);
389 assert_eq!(members[0].0, pubkey_hex(&owner));
390 }
391
392 #[test]
393 fn is_member_at_tracks_over_time() {
394 let owner = gen_keypair();
395 let member = gen_keypair();
396
397 let chain = MembershipChain::from_entries(vec![
398 founder_entry(&owner, "0000000001000-0000-dev1"),
399 make_entry(
400 &owner,
401 MembershipAction::Add,
402 &member,
403 MemberRole::Member,
404 "0000000002000-0000-dev1",
405 ),
406 make_entry(
407 &owner,
408 MembershipAction::Remove,
409 &member,
410 MemberRole::Member,
411 "0000000004000-0000-dev1",
412 ),
413 ])
414 .unwrap();
415
416 assert!(chain.is_member_at(&pubkey_hex(&owner), "0000000001000-0000-dev1"));
418 assert!(chain.is_member_at(&pubkey_hex(&owner), "0000000005000-0000-dev1"));
419
420 assert!(!chain.is_member_at(&pubkey_hex(&member), "0000000000500-0000-dev1"));
422 assert!(chain.is_member_at(&pubkey_hex(&member), "0000000002000-0000-dev1"));
423 assert!(chain.is_member_at(&pubkey_hex(&member), "0000000003000-0000-dev1"));
424 assert!(!chain.is_member_at(&pubkey_hex(&member), "0000000004000-0000-dev1"));
425 }
426
427 #[test]
428 fn current_members_returns_active() {
429 let owner = gen_keypair();
430 let m1 = gen_keypair();
431 let m2 = gen_keypair();
432
433 let chain = MembershipChain::from_entries(vec![
434 founder_entry(&owner, "0000000001000-0000-dev1"),
435 make_entry(
436 &owner,
437 MembershipAction::Add,
438 &m1,
439 MemberRole::Member,
440 "0000000002000-0000-dev1",
441 ),
442 make_entry(
443 &owner,
444 MembershipAction::Add,
445 &m2,
446 MemberRole::Member,
447 "0000000003000-0000-dev1",
448 ),
449 make_entry(
450 &owner,
451 MembershipAction::Remove,
452 &m1,
453 MemberRole::Member,
454 "0000000004000-0000-dev1",
455 ),
456 ])
457 .unwrap();
458
459 let members = chain.current_members();
460 assert_eq!(members.len(), 2);
461 assert!(members.iter().any(|(pk, _)| pk == &pubkey_hex(&owner)));
462 assert!(members.iter().any(|(pk, _)| pk == &pubkey_hex(&m2)));
463 assert!(!members.iter().any(|(pk, _)| pk == &pubkey_hex(&m1)));
464 }
465
466 #[test]
467 fn add_signed_by_non_owner_fails() {
468 let owner = gen_keypair();
469 let member = gen_keypair();
470 let outsider = gen_keypair();
471
472 let mut chain = MembershipChain::new();
473 chain
474 .add_entry(founder_entry(&owner, "0000000001000-0000-dev1"))
475 .unwrap();
476 chain
477 .add_entry(make_entry(
478 &owner,
479 MembershipAction::Add,
480 &member,
481 MemberRole::Member,
482 "0000000002000-0000-dev1",
483 ))
484 .unwrap();
485
486 let result = chain.add_entry(make_entry(
488 &member,
489 MembershipAction::Add,
490 &outsider,
491 MemberRole::Member,
492 "0000000003000-0000-dev1",
493 ));
494
495 assert!(matches!(result, Err(MembershipError::NotAnOwner(_))));
496 }
497
498 #[test]
499 fn remove_signed_by_non_owner_fails() {
500 let owner = gen_keypair();
501 let m1 = gen_keypair();
502 let m2 = gen_keypair();
503
504 let mut chain = MembershipChain::new();
505 chain
506 .add_entry(founder_entry(&owner, "0000000001000-0000-dev1"))
507 .unwrap();
508 chain
509 .add_entry(make_entry(
510 &owner,
511 MembershipAction::Add,
512 &m1,
513 MemberRole::Member,
514 "0000000002000-0000-dev1",
515 ))
516 .unwrap();
517 chain
518 .add_entry(make_entry(
519 &owner,
520 MembershipAction::Add,
521 &m2,
522 MemberRole::Member,
523 "0000000003000-0000-dev1",
524 ))
525 .unwrap();
526
527 let result = chain.add_entry(make_entry(
529 &m1,
530 MembershipAction::Remove,
531 &m2,
532 MemberRole::Member,
533 "0000000004000-0000-dev1",
534 ));
535
536 assert!(matches!(result, Err(MembershipError::NotAnOwner(_))));
537 }
538
539 #[test]
540 fn first_entry_not_self_signed_owner_add_fails() {
541 let kp1 = gen_keypair();
542 let kp2 = gen_keypair();
543
544 let entry = make_entry(
546 &kp1,
547 MembershipAction::Add,
548 &kp2,
549 MemberRole::Owner,
550 "0000000001000-0000-dev1",
551 );
552
553 let mut chain = MembershipChain::new();
554 let result = chain.add_entry(entry);
555 assert!(matches!(result, Err(MembershipError::InvalidFirstEntry)));
556 }
557
558 #[test]
559 fn first_entry_as_member_fails() {
560 let kp = gen_keypair();
561 let pk_hex = pubkey_hex(&kp);
562
563 let mut entry = MembershipEntry {
565 action: MembershipAction::Add,
566 user_pubkey: pk_hex.clone(),
567 role: MemberRole::Member,
568 timestamp: "0000000001000-0000-dev1".to_string(),
569 author_pubkey: pk_hex,
570 signature: String::new(),
571 };
572 sign_membership_entry(&mut entry, &kp);
573
574 let mut chain = MembershipChain::new();
575 let result = chain.add_entry(entry);
576 assert!(matches!(result, Err(MembershipError::InvalidFirstEntry)));
577 }
578
579 #[test]
580 fn tampered_entry_fails_signature_verification() {
581 let owner = gen_keypair();
582 let member = gen_keypair();
583
584 let mut entry = make_entry(
585 &owner,
586 MembershipAction::Add,
587 &member,
588 MemberRole::Member,
589 "0000000002000-0000-dev1",
590 );
591
592 entry.role = MemberRole::Owner;
594
595 assert!(!verify_membership_entry(&entry));
596
597 let mut chain = MembershipChain::new();
599 chain
600 .add_entry(founder_entry(&owner, "0000000001000-0000-dev1"))
601 .unwrap();
602
603 let result = chain.add_entry(entry);
604 assert!(matches!(result, Err(MembershipError::InvalidSignature(_))));
605 }
606
607 #[test]
608 fn entry_ordering_by_timestamp() {
609 let owner = gen_keypair();
610 let m1 = gen_keypair();
611 let m2 = gen_keypair();
612
613 let e3 = make_entry(
615 &owner,
616 MembershipAction::Add,
617 &m2,
618 MemberRole::Member,
619 "0000000003000-0000-dev1",
620 );
621 let e1 = founder_entry(&owner, "0000000001000-0000-dev1");
622 let e2 = make_entry(
623 &owner,
624 MembershipAction::Add,
625 &m1,
626 MemberRole::Member,
627 "0000000002000-0000-dev1",
628 );
629
630 let chain = MembershipChain::from_entries(vec![e3, e1, e2]).unwrap();
632
633 let entries = chain.entries();
635 assert!(entries[0].timestamp < entries[1].timestamp);
636 assert!(entries[1].timestamp < entries[2].timestamp);
637 }
638
639 #[test]
640 fn validate_empty_chain_fails() {
641 let chain = MembershipChain::new();
642 let result = chain.validate();
643 assert!(matches!(result, Err(MembershipError::EmptyChain)));
644 }
645
646 #[test]
647 fn validate_detects_invalid_signature_in_middle() {
648 let owner = gen_keypair();
649 let member = gen_keypair();
650
651 let e1 = founder_entry(&owner, "0000000001000-0000-dev1");
652 let mut e2 = make_entry(
653 &owner,
654 MembershipAction::Add,
655 &member,
656 MemberRole::Member,
657 "0000000002000-0000-dev1",
658 );
659
660 e2.timestamp = "0000000002500-0000-dev1".to_string();
662
663 let result = MembershipChain::from_entries(vec![e1, e2]);
664 assert!(matches!(result, Err(MembershipError::InvalidSignature(1))));
665 }
666
667 #[test]
668 fn canonical_bytes_is_deterministic() {
669 let kp = gen_keypair();
670 let entry = MembershipEntry {
671 action: MembershipAction::Add,
672 user_pubkey: pubkey_hex(&kp),
673 role: MemberRole::Owner,
674 timestamp: "0000000001000-0000-dev1".to_string(),
675 author_pubkey: pubkey_hex(&kp),
676 signature: "does-not-matter".to_string(),
677 };
678
679 let b1 = canonical_bytes(&entry);
680 let b2 = canonical_bytes(&entry);
681 assert_eq!(b1, b2);
682
683 let mut entry2 = entry.clone();
685 entry2.signature = "something-else".to_string();
686 assert_eq!(canonical_bytes(&entry2), b1);
687 }
688}