1use std::fmt;
4use std::str::FromStr;
5
6use hkdf::Hkdf;
7use hmac::{Hmac, Mac};
8use serde::{Deserialize, Deserializer, Serialize, Serializer};
9use sha2::Sha256;
10
11use super::membership::MembershipGrantId;
12use super::store_commit::ObjectHash;
13use crate::encryption::EncryptionService;
14
15pub use super::circle_control::*;
16pub use super::circle_roster::*;
17
18const CIRCLE_ID_ALPHABET: &[u8; 32] = b"abcdefghijklmnopqrstuvwxyz234567";
19const CIRCLE_ID_LENGTH: usize = 26;
20const ROW_ROUTING_KEY_DOMAIN: &[u8] = b"coven.row-routing.v1";
21const ROW_ROUTING_ID_DOMAIN: &[u8] = b"coven.row-routing-id.v1\0";
22const CIRCLE_ID_FOUNDER_DOMAIN: &str = "coven.circle-id-founder.v1";
23const CIRCLE_EPOCH_ID_GENERATION_DOMAIN: &[u8] = b"coven.circle-epoch-id-generation.v1\0";
24const ACCESS_LEAF_ID_GENERATION_DOMAIN: &[u8] = b"coven.circle-access-leaf-id-generation.v1\0";
25
26pub const CIRCLE_CONTROL_PREFIX: &str = "circle-control/";
27pub const CIRCLE_ROSTER_PREFIX: &str = "circles/";
28pub const CIRCLE_METADATA_PREFIX: &str = "circles/";
29pub const CIRCLE_ACCESS_LEAF_PREFIX: &str = "circles/";
30pub const CIRCLE_ACCESS_ENVELOPE_PREFIX: &str = "circles/";
31
32#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
34pub struct CircleId([u8; 16]);
35
36impl CircleId {
37 pub(crate) fn founder(
38 store_root_hash: ObjectHash,
39 author_pubkey: &str,
40 owner_grant: &MembershipGrantId,
41 ) -> Self {
42 #[derive(Serialize)]
43 struct Founder<'a> {
44 domain: &'static str,
45 store_root_hash: ObjectHash,
46 author_pubkey: &'a str,
47 owner_grant: &'a MembershipGrantId,
48 }
49 let digest = ObjectHash::digest(
50 &serde_json::to_vec(&Founder {
51 domain: CIRCLE_ID_FOUNDER_DOMAIN,
52 store_root_hash,
53 author_pubkey,
54 owner_grant,
55 })
56 .expect("Circle ID founder serialization cannot fail"),
57 );
58 let mut bytes = [0_u8; 16];
59 bytes.copy_from_slice(&digest.as_bytes()[..16]);
60 Self(bytes)
61 }
62
63 pub fn from_bytes(bytes: [u8; 16]) -> Self {
64 Self(bytes)
65 }
66
67 pub fn as_bytes(&self) -> &[u8; 16] {
68 &self.0
69 }
70}
71
72impl fmt::Debug for CircleId {
73 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
74 fmt::Display::fmt(self, formatter)
75 }
76}
77
78impl fmt::Display for CircleId {
79 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
80 formatter.write_str(&encode_base32(&self.0))
81 }
82}
83
84impl FromStr for CircleId {
85 type Err = CircleIdError;
86
87 fn from_str(value: &str) -> Result<Self, Self::Err> {
88 let bytes = decode_base32(value)?;
89 let id = Self(bytes);
90 if id.to_string() != value || value == "local" {
91 return Err(CircleIdError(value.to_string()));
92 }
93 Ok(id)
94 }
95}
96
97impl Serialize for CircleId {
98 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
99 where
100 S: Serializer,
101 {
102 serializer.serialize_str(&self.to_string())
103 }
104}
105
106impl<'de> Deserialize<'de> for CircleId {
107 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
108 where
109 D: Deserializer<'de>,
110 {
111 String::deserialize(deserializer)?
112 .parse()
113 .map_err(serde::de::Error::custom)
114 }
115}
116
117#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
118#[error("circle id must be canonical 128-bit lowercase base32: {0:?}")]
119pub struct CircleIdError(String);
120
121#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
122#[serde(rename_all = "snake_case")]
123pub enum CircleRole {
124 Owner,
125 Member,
126}
127
128#[derive(Debug, Clone, PartialEq, Eq)]
129pub struct CircleInfo {
130 pub id: CircleId,
131 pub name: String,
132 pub role: CircleRole,
133}
134
135#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
136#[serde(rename_all = "snake_case", deny_unknown_fields)]
137pub enum CircleOperationState {
138 Pending,
139 Blocked { reason: String },
140}
141
142#[derive(Debug, Clone, PartialEq, Eq)]
143pub struct CircleOperationInfo {
144 pub circle_id: CircleId,
145 pub name: String,
146 pub state: CircleOperationState,
147}
148
149#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
151pub enum Audience {
152 Store,
153 Circle(CircleId),
154 Local,
155}
156
157impl Audience {
158 pub fn from_column(value: Option<&str>) -> Result<Self, CircleIdError> {
159 match value {
160 None => Ok(Self::Store),
161 Some("local") => Ok(Self::Local),
162 Some(circle) => circle.parse().map(Self::Circle),
163 }
164 }
165
166 pub fn column_value(&self) -> Option<String> {
167 match self {
168 Self::Store => None,
169 Self::Circle(circle) => Some(circle.to_string()),
170 Self::Local => Some("local".to_string()),
171 }
172 }
173}
174
175macro_rules! generated_hex_id {
176 ($name:ident, $domain:ident) => {
177 #[derive(
178 Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
179 )]
180 #[serde(transparent)]
181 pub struct $name([u8; 16]);
182
183 impl $name {
184 pub(crate) fn generate(ids: &dyn crate::id_provider::IdProvider) -> Self {
185 Self(generated_id_bytes(ids, $domain))
186 }
187 }
188
189 impl fmt::Display for $name {
190 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
191 formatter.write_str(&hex::encode(self.0))
192 }
193 }
194 };
195}
196
197generated_hex_id!(CircleEpochId, CIRCLE_EPOCH_ID_GENERATION_DOMAIN);
198generated_hex_id!(AccessLeafId, ACCESS_LEAF_ID_GENERATION_DOMAIN);
199
200pub(crate) fn generated_id_digest(
201 ids: &dyn crate::id_provider::IdProvider,
202 domain: &[u8],
203) -> ObjectHash {
204 let id = ids.new_id();
205 let mut material = Vec::with_capacity(domain.len() + id.len());
206 material.extend_from_slice(domain);
207 material.extend_from_slice(id.as_bytes());
208 ObjectHash::digest(&material)
209}
210
211fn generated_id_bytes(ids: &dyn crate::id_provider::IdProvider, domain: &[u8]) -> [u8; 16] {
212 generated_id_digest(ids, domain).as_bytes()[..16]
213 .try_into()
214 .expect("SHA-256 digest prefix has fixed length")
215}
216
217#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
220pub struct RowRoutingId([u8; 32]);
221
222impl fmt::Debug for RowRoutingId {
223 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
224 fmt::Display::fmt(self, formatter)
225 }
226}
227
228impl fmt::Display for RowRoutingId {
229 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
230 formatter.write_str(&hex::encode(self.0))
231 }
232}
233
234impl FromStr for RowRoutingId {
235 type Err = RowRoutingIdError;
236
237 fn from_str(value: &str) -> Result<Self, Self::Err> {
238 if value.len() != 64
239 || value
240 .bytes()
241 .any(|byte| !byte.is_ascii_digit() && !(b'a'..=b'f').contains(&byte))
242 {
243 return Err(RowRoutingIdError(value.to_string()));
244 }
245 let bytes: [u8; 32] = hex::decode(value)
246 .map_err(|_| RowRoutingIdError(value.to_string()))?
247 .try_into()
248 .map_err(|_| RowRoutingIdError(value.to_string()))?;
249 Ok(Self(bytes))
250 }
251}
252
253impl Serialize for RowRoutingId {
254 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
255 where
256 S: Serializer,
257 {
258 serializer.serialize_str(&self.to_string())
259 }
260}
261
262impl<'de> Deserialize<'de> for RowRoutingId {
263 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
264 where
265 D: Deserializer<'de>,
266 {
267 String::deserialize(deserializer)?
268 .parse()
269 .map_err(serde::de::Error::custom)
270 }
271}
272
273#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
274#[error("row routing id must be exactly 64 lowercase hexadecimal characters: {0:?}")]
275pub struct RowRoutingIdError(String);
276
277#[derive(Clone)]
278pub(crate) struct RowRoutingKey([u8; 32]);
279
280#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
281pub enum RowRoutingKeyError {
282 #[error("Store keyring has no generation-1 key")]
283 MissingGenerationOne,
284 #[error("Store keyring has more than one generation-1 key")]
285 AmbiguousGenerationOne,
286}
287
288pub(crate) fn derive_row_routing_key(
289 encryption: &EncryptionService,
290 store_root_hash: ObjectHash,
291) -> Result<RowRoutingKey, RowRoutingKeyError> {
292 let mut generation_one = encryption
293 .keyring_entries()
294 .into_iter()
295 .filter_map(|(generation, key)| (generation == 1).then_some(key));
296 let key = generation_one
297 .next()
298 .ok_or(RowRoutingKeyError::MissingGenerationOne)?;
299 if generation_one.next().is_some() {
300 return Err(RowRoutingKeyError::AmbiguousGenerationOne);
301 }
302 let hkdf = Hkdf::<Sha256>::new(Some(ROW_ROUTING_KEY_DOMAIN), &key);
303 let mut derived = [0u8; 32];
304 hkdf.expand(store_root_hash.as_bytes(), &mut derived)
305 .expect("32 bytes is a valid HKDF output length");
306 Ok(RowRoutingKey(derived))
307}
308
309pub(crate) fn row_routing_id(key: &RowRoutingKey, table: &str, row_id: &str) -> RowRoutingId {
310 let mut mac = Hmac::<Sha256>::new_from_slice(&key.0).expect("HMAC accepts a 32-byte key");
311 mac.update(ROW_ROUTING_ID_DOMAIN);
312 mac.update(&(table.len() as u64).to_be_bytes());
313 mac.update(table.as_bytes());
314 mac.update(&(row_id.len() as u64).to_be_bytes());
315 mac.update(row_id.as_bytes());
316 RowRoutingId(mac.finalize().into_bytes().into())
317}
318
319fn encode_base32(bytes: &[u8; 16]) -> String {
320 let mut output = String::with_capacity(CIRCLE_ID_LENGTH);
321 let mut buffer = 0u32;
322 let mut bits = 0u8;
323 for byte in bytes {
324 buffer = (buffer << 8) | u32::from(*byte);
325 bits += 8;
326 while bits >= 5 {
327 bits -= 5;
328 output.push(CIRCLE_ID_ALPHABET[((buffer >> bits) & 0x1f) as usize] as char);
329 buffer &= (1u32 << bits).wrapping_sub(1);
330 }
331 }
332 if bits != 0 {
333 output.push(CIRCLE_ID_ALPHABET[((buffer << (5 - bits)) & 0x1f) as usize] as char);
334 }
335 output
336}
337
338fn decode_base32(value: &str) -> Result<[u8; 16], CircleIdError> {
339 if value.len() != CIRCLE_ID_LENGTH {
340 return Err(CircleIdError(value.to_string()));
341 }
342 let mut output = Vec::with_capacity(16);
343 let mut buffer = 0u32;
344 let mut bits = 0u8;
345 for byte in value.bytes() {
346 let digit = CIRCLE_ID_ALPHABET
347 .iter()
348 .position(|candidate| *candidate == byte)
349 .ok_or_else(|| CircleIdError(value.to_string()))? as u32;
350 buffer = (buffer << 5) | digit;
351 bits += 5;
352 while bits >= 8 {
353 bits -= 8;
354 output.push(((buffer >> bits) & 0xff) as u8);
355 buffer &= (1u32 << bits).wrapping_sub(1);
356 }
357 }
358 if output.len() != 16 || buffer != 0 {
359 return Err(CircleIdError(value.to_string()));
360 }
361 output
362 .try_into()
363 .map_err(|_| CircleIdError(value.to_string()))
364}
365
366#[cfg(test)]
367mod tests {
368 use std::collections::BTreeMap;
369
370 use super::super::circle_control::{merkle_root_and_proofs, verify_merkle_proof};
371 use super::*;
372 use crate::keys::{self, UserKeypair};
373
374 fn candidate_family(label: &str) -> super::super::store_commit::CandidateFamilyId {
375 super::super::store_commit::CandidateFamilyId::from_hash(ObjectHash::digest(
376 label.as_bytes(),
377 ))
378 }
379
380 fn exact_object(label: &str, bytes: &[u8]) -> super::super::storage::ExactObjectRef {
381 super::super::storage::ExactObjectRef::new(
382 crate::storage::cloud::ObjectSlot::logical(format!("store-v1/test/{label}.json"))
383 .unwrap(),
384 bytes.len() as u64,
385 ObjectHash::digest(bytes),
386 )
387 }
388
389 fn exact_logical_object(
390 logical_key: String,
391 bytes: &[u8],
392 ) -> super::super::storage::ExactObjectRef {
393 super::super::storage::ExactObjectRef::new(
394 crate::storage::cloud::ObjectSlot::logical(logical_key).unwrap(),
395 bytes.len() as u64,
396 ObjectHash::digest(bytes),
397 )
398 }
399
400 fn test_founder_entry(
401 label: &str,
402 owner: &UserKeypair,
403 membership: super::super::store_commit::GrantStreamAnchor,
404 ) -> super::super::membership::MembershipEntry {
405 super::super::membership::founder_entry(
406 label,
407 owner,
408 crate::sync::test_helpers::test_membership_grant_id(label),
409 "founder",
410 membership,
411 crate::sync::test_helpers::test_founder_provider_admin(label),
412 )
413 }
414
415 fn merge_membership_ref(
416 owner: &UserKeypair,
417 members: &[(String, super::super::membership::MemberRole)],
418 label: &str,
419 ) -> (
420 StoreMembershipStateRef,
421 super::super::membership::MembershipGrantCreationAuthority,
422 ) {
423 let founder = test_founder_entry(
424 label,
425 owner,
426 super::super::store_commit::GrantStreamAnchor::StoreMembership {
427 first_slot: crate::storage::cloud::ObjectSlot::logical(format!(
428 "store-v1/test/{label}/membership/1.json"
429 ))
430 .unwrap(),
431 },
432 );
433 let founder_coord = founder.coord();
434 let mut chain = super::super::membership::MembershipChain::from_entries(vec![founder])
435 .expect("found merge-concurrent membership");
436 for (index, (pubkey, role)) in members.iter().enumerate() {
437 if pubkey == &keys::public_key_hex(owner) {
438 continue;
439 }
440 let entry = chain
441 .signed_set_member_in_stream(
442 owner,
443 founder_coord.stream_id,
444 pubkey.clone(),
445 None,
446 role.clone(),
447 format!("member-{index}"),
448 )
449 .expect("sign merge-concurrent member");
450 chain
451 .add_entry(entry)
452 .expect("apply merge-concurrent member");
453 }
454 let resolved = match chain.status() {
455 super::super::membership::MembershipStatus::Resolved(resolved) => resolved,
456 super::super::membership::MembershipStatus::Conflict(_) => {
457 panic!("membership fixture must resolve")
458 }
459 };
460 let tip = chain.entries().last().expect("membership tip").coord();
461 let head = super::super::membership::MembershipHeadRef {
462 coord: tip,
463 head_hash: ObjectHash::digest(format!("{label} head").as_bytes()),
464 object: exact_object(&format!("{label}/membership-head"), b"membership head"),
465 };
466 (
467 StoreMembershipStateRef::merge_concurrent(
468 vec![head],
469 Vec::new(),
470 Vec::new(),
471 resolved.state_hash,
472 )
473 .expect("valid merge-concurrent membership reference"),
474 super::super::membership::MembershipGrantCreationAuthority::Entry(founder_coord),
475 )
476 }
477
478 fn serial_membership_ref(
479 owner: &UserKeypair,
480 members: &[(String, super::super::membership::MemberRole)],
481 label: &str,
482 ) -> StoreMembershipStateRef {
483 let root_bytes = format!("{label} root").into_bytes();
484 let root = super::super::store_commit::StoreRootRef {
485 store_root_id: ObjectHash::digest(format!("{label} identity").as_bytes()),
486 store_root_hash: ObjectHash::digest(&root_bytes),
487 object: exact_object(&format!("{label}/root"), &root_bytes),
488 };
489 let origin = super::super::store_commit::StoreDeviceRegistrationOrigin::Founder {
490 creation_id: super::super::store_commit::StoreCreationId::from_nonce(label),
491 };
492 let founder_registration = super::super::store_commit::StoreDeviceRegistrationRef {
493 device_id: super::super::store_commit::StoreDeviceId::derive(&root, &origin),
494 registration_hash: ObjectHash::digest(format!("{label} registration").as_bytes()),
495 object: exact_object(
496 &format!("{label}/registration"),
497 format!("{label} registration").as_bytes(),
498 ),
499 };
500 let founder = test_founder_entry(
501 label,
502 owner,
503 super::super::store_commit::GrantStreamAnchor::StoreMembership {
504 first_slot: crate::storage::cloud::ObjectSlot::logical(format!(
505 "store-v1/test/{label}/membership/1.json"
506 ))
507 .unwrap(),
508 },
509 );
510 let mut membership = super::super::membership::SerialMembershipState::from_founder(
511 root.store_root_id,
512 &founder,
513 )
514 .expect("found Serial membership");
515 for (index, (pubkey, role)) in members.iter().enumerate() {
516 if pubkey == &keys::public_key_hex(owner) {
517 continue;
518 }
519 let entry = membership
520 .signed_set_member(
521 owner,
522 pubkey.clone(),
523 None,
524 role.clone(),
525 format!("member-{index}"),
526 )
527 .expect("sign Serial member");
528 membership = membership.apply(&entry).expect("apply Serial member");
529 }
530 let authorization =
531 super::super::membership::SerialAuthorizationState::from_test_membership(
532 &founder, membership,
533 )
534 .expect("test Serial authorization");
535 StoreMembershipStateRef::serial(
536 super::super::store_commit::SerialStorePosition::Genesis {
537 root,
538 founder_registration,
539 },
540 Vec::new(),
541 &authorization,
542 )
543 .expect("valid Serial membership reference")
544 }
545
546 struct MergeDeviceAuthority {
547 registration: super::super::store_commit::StoreDeviceRegistration,
548 reference: super::super::store_commit::StoreDeviceRegistrationRef,
549 device_signer: UserKeypair,
550 stream_id: super::super::membership::AuthorStreamId,
551 }
552
553 fn merge_device_authority(
554 identity: &UserKeypair,
555 store_root_hash: ObjectHash,
556 label: &str,
557 ) -> MergeDeviceAuthority {
558 let root = super::super::store_commit::StoreRootRef {
559 store_root_id: ObjectHash::digest(format!("{label} identity").as_bytes()),
560 store_root_hash,
561 object: exact_object(&format!("{label}/root"), label.as_bytes()),
562 };
563 let slot = |stream: &str| {
564 crate::storage::cloud::ObjectSlot::logical(format!(
565 "store-v1/test/{label}/{stream}/1.json"
566 ))
567 .unwrap()
568 };
569 let registration = super::super::store_commit::StoreDeviceRegistration::signed(
570 root.clone(),
571 super::super::store_commit::StoreDeviceRegistrationOrigin::Founder {
572 creation_id: super::super::store_commit::StoreCreationId::from_nonce(label),
573 },
574 super::super::storage::ProviderDeviceBinding {
575 principal: super::super::storage::ProviderPrincipalId::CustomS3Credential {
576 access_key_id_hash: ObjectHash::digest(label.as_bytes()),
577 },
578 },
579 super::super::store_commit::StoreCommitAnchor::MergeConcurrent {
580 announcements: super::super::store_commit::DeviceStreamAnchor::StoreAnnouncements {
581 first_slot: slot("announcements"),
582 },
583 },
584 super::super::store_commit::DeviceStreamAnchor::StoreAcknowledgements {
585 first_slot: slot("acknowledgements"),
586 },
587 super::super::store_commit::DeviceStreamAnchor::StoreSnapshots {
588 first_slot: slot("snapshots"),
589 },
590 identity,
591 )
592 .expect("sign test device registration");
593 let bytes = registration.to_bytes();
594 let reference = super::super::store_commit::StoreDeviceRegistrationRef::from_registration(
595 ®istration,
596 exact_object(&format!("{label}/registration"), &bytes),
597 );
598 let device_signer = registration
599 .device_signer(identity)
600 .expect("derive registered device signer");
601 let stream_id =
602 super::super::membership::AuthorStreamId::store_announcements(&root, &reference);
603 MergeDeviceAuthority {
604 registration,
605 reference,
606 device_signer,
607 stream_id,
608 }
609 }
610
611 fn test_activation_objects(label: &str) -> super::super::store_commit::CircleActivationObjects {
612 let bytes = label.as_bytes();
613 super::super::store_commit::CircleActivationObjects {
614 control: super::super::storage::ExactObjectRef::new(
615 crate::storage::cloud::ObjectSlot::logical(format!(
616 "store-v1/test-circle-controls/{label}.json"
617 ))
618 .expect("test control slot is valid"),
619 bytes.len() as u64,
620 ObjectHash::digest(bytes),
621 ),
622 control_head: None,
623 roster_entries: BTreeMap::new(),
624 roster_heads: BTreeMap::new(),
625 roster_resolutions: BTreeMap::new(),
626 metadata_entries: BTreeMap::new(),
627 metadata_heads: BTreeMap::new(),
628 access: Vec::new(),
629 }
630 }
631
632 #[test]
633 fn merkle_proofs_verify_for_every_leaf_in_even_and_odd_layers() {
634 for leaf_count in 1..=9 {
635 let leaves = (0..leaf_count)
636 .map(|index| ObjectHash::digest(format!("leaf-{index}").as_bytes()))
637 .collect::<Vec<_>>();
638 let (root, proofs) = merkle_root_and_proofs(&leaves);
639 assert_eq!(proofs.len(), leaves.len());
640 for (index, (leaf, proof)) in leaves.iter().zip(&proofs).enumerate() {
641 assert!(
642 verify_merkle_proof(*leaf, proof, root),
643 "leaf {index} of {leaf_count} failed its canonical proof"
644 );
645 }
646 }
647 }
648
649 #[test]
650 fn founder_payload_is_complete_and_acyclic_for_both_store_policies() {
651 let owner = crate::keys::UserKeypair::generate();
652 let peer = crate::keys::UserKeypair::generate();
653 let owner_pubkey = crate::keys::public_key_hex(&owner);
654 let peer_pubkey = crate::keys::public_key_hex(&peer);
655 let members = vec![
656 (
657 owner_pubkey.clone(),
658 super::super::membership::MemberRole::Owner,
659 ),
660 (
661 peer_pubkey.clone(),
662 super::super::membership::MemberRole::Member,
663 ),
664 ];
665
666 let (merge_membership, merge_authority) =
667 merge_membership_ref(&owner, &members, "founder-circle-merge");
668 for (membership, membership_authority) in [
669 (merge_membership, Some(merge_authority)),
670 (
671 serial_membership_ref(&owner, &members, "founder-circle-serial"),
672 None,
673 ),
674 ] {
675 let ids = crate::id_provider::SequentialIdProvider::new("founder-circle");
676 let candidate_family = candidate_family("founder-circle");
677 let creation = CircleCreation::founder(
678 ObjectHash::digest(b"store-root"),
679 candidate_family,
680 "device-a",
681 "Household",
682 "0000000001000-0000-device-a",
683 membership,
684 membership_authority,
685 members.clone(),
686 &ids,
687 &owner,
688 )
689 .expect("construct founder circle");
690
691 assert!(creation.control.verify());
692 assert!(creation.metadata.verify());
693 assert!(creation.resolved_roster().verify());
694 assert_eq!(creation.access.len(), 2);
695 for access in &creation.access {
696 assert!(access.leaf.verify(&creation.control, candidate_family));
697 assert!(access.envelope.verify(&creation.control, candidate_family));
698 assert!(access.leaf.verify_envelope(
699 &creation.control,
700 &access.envelope,
701 candidate_family
702 ));
703 assert!(!access.leaf.bytes.windows(64).any(|window| {
704 window == creation.control.coord.control_hash().to_string().as_bytes()
705 }));
706 }
707 assert!(matches!(
708 creation
709 .access
710 .iter()
711 .find(|access| access.leaf.value.recipient_pubkey == owner_pubkey)
712 .unwrap()
713 .leaf
714 .value
715 .disposition,
716 CircleAccessDisposition::Active { .. }
717 ));
718 assert!(matches!(
719 creation
720 .access
721 .iter()
722 .find(|access| access.leaf.value.recipient_pubkey == peer_pubkey)
723 .unwrap()
724 .leaf
725 .value
726 .disposition,
727 CircleAccessDisposition::Inactive
728 ));
729
730 if matches!(
731 creation.control.value.order,
732 CircleControlOrder::MergeConcurrent { .. }
733 ) {
734 let mut seized = creation.control.value.clone();
735 seized.circle_id = CircleId::from_bytes([0x5a; 16]);
736 seized.signature = keys::sign_hex(&owner, &seized.canonical_bytes()).1;
737 assert!(
738 !seized.verify(),
739 "a founder control must not choose an arbitrary Circle ID"
740 );
741
742 let mut discontinuous = creation.control.value.clone();
743 let CircleControlOrder::MergeConcurrent { seq, .. } = &mut discontinuous.order
744 else {
745 unreachable!()
746 };
747 *seq = 2;
748 discontinuous.signature =
749 keys::sign_hex(&owner, &discontinuous.canonical_bytes()).1;
750 assert!(
751 !discontinuous.verify(),
752 "a control without a predecessor must be genesis"
753 );
754 }
755 }
756 }
757
758 #[test]
759 fn access_verification_rejects_signed_context_and_proof_substitution() {
760 let owner = crate::keys::UserKeypair::generate();
761 let peer = crate::keys::UserKeypair::generate();
762 let owner_pubkey = crate::keys::public_key_hex(&owner);
763 let peer_pubkey = crate::keys::public_key_hex(&peer);
764 let members = vec![
765 (
766 owner_pubkey.clone(),
767 super::super::membership::MemberRole::Owner,
768 ),
769 (
770 peer_pubkey.clone(),
771 super::super::membership::MemberRole::Member,
772 ),
773 ];
774 let (membership, authority) = merge_membership_ref(&owner, &members, "access-verification");
775 let ids = crate::id_provider::SequentialIdProvider::new("access-verification");
776 let candidate_family = candidate_family("access-verification");
777 let creation = CircleCreation::founder(
778 ObjectHash::digest(b"store-root"),
779 candidate_family,
780 "device-a",
781 "Household",
782 "0000000001000-0000-device-a",
783 membership,
784 Some(authority),
785 members.clone(),
786 &ids,
787 &owner,
788 )
789 .expect("construct founder circle");
790
791 let mut wrong_store = creation.access[0].envelope.clone();
792 wrong_store.store_root_hash = ObjectHash::digest(b"other-store");
793
794 wrong_store.signature = keys::sign_hex(&owner, &wrong_store.canonical_bytes()).1;
795 assert!(!wrong_store.verify(&creation.control, candidate_family));
796
797 let mut wrong_family_envelope = creation.access[0].envelope.clone();
798 wrong_family_envelope.candidate_family =
799 super::super::store_commit::CandidateFamilyId::from_hash(ObjectHash::digest(
800 b"other access family",
801 ));
802 wrong_family_envelope.signature =
803 keys::sign_hex(&owner, &wrong_family_envelope.canonical_bytes()).1;
804 assert!(!wrong_family_envelope.verify(&creation.control, candidate_family));
805
806 let mut wrong_family_leaf = creation.access[0].leaf.clone();
807 wrong_family_leaf.value.candidate_family =
808 super::super::store_commit::CandidateFamilyId::from_hash(ObjectHash::digest(
809 b"other leaf family",
810 ));
811 wrong_family_leaf.value.signature =
812 keys::sign_hex(&owner, &wrong_family_leaf.value.canonical_bytes()).1;
813 assert!(!wrong_family_leaf.verify(&creation.control, candidate_family));
814
815 let mut non_owner = creation.access[0].envelope.clone();
816 non_owner.owner_pubkey = peer_pubkey;
817 non_owner.signature = keys::sign_hex(&peer, &non_owner.canonical_bytes()).1;
818 assert!(!non_owner.verify(&creation.control, candidate_family));
819
820 let mut substituted_proof = creation.access[0].envelope.clone();
821 substituted_proof.proof = creation.access[1].envelope.proof.clone();
822 substituted_proof.signature =
823 keys::sign_hex(&owner, &substituted_proof.canonical_bytes()).1;
824 assert!(!substituted_proof.verify(&creation.control, candidate_family));
825
826 let mut substituted_leaf_id = creation.access[0].envelope.clone();
827 substituted_leaf_id.leaf_id = creation.access[1].leaf.value.leaf_id;
828 substituted_leaf_id.signature =
829 keys::sign_hex(&owner, &substituted_leaf_id.canonical_bytes()).1;
830 assert!(substituted_leaf_id.verify(&creation.control, candidate_family));
831 assert!(!creation.access[0].leaf.verify_envelope(
832 &creation.control,
833 &substituted_leaf_id,
834 candidate_family,
835 ));
836
837 let mut wrong_membership_leaf = creation.access[0].leaf.value.clone();
838 wrong_membership_leaf.store_membership =
839 serial_membership_ref(&owner, &members, "wrong-membership-leaf");
840 wrong_membership_leaf.signature =
841 keys::sign_hex(&owner, &wrong_membership_leaf.canonical_bytes()).1;
842 let recipient_key =
843 keys::ed25519_to_x25519_public_key(&owner.public_key()).expect("convert recipient key");
844 let bytes = keys::seal_box_encrypt(
845 &serde_json::to_vec(&wrong_membership_leaf).expect("serialize forged leaf"),
846 &recipient_key,
847 );
848 let wrong_membership_leaf = PreparedAccessLeaf {
849 leaf_hash: ObjectHash::digest(&bytes),
850 bytes,
851 value: wrong_membership_leaf,
852 };
853 assert!(!wrong_membership_leaf.verify(&creation.control, candidate_family));
854
855 let mut wrong_keyring_leaf = creation
856 .access
857 .iter()
858 .find(|access| {
859 matches!(
860 &access.leaf.value.disposition,
861 CircleAccessDisposition::Active { .. }
862 )
863 })
864 .expect("founder access")
865 .leaf
866 .value
867 .clone();
868 let CircleAccessDisposition::Active { keyring, .. } = &mut wrong_keyring_leaf.disposition
869 else {
870 panic!("founder access must be active")
871 };
872 *keyring = crate::encryption::MasterKeyring::generate().to_serialized();
873 wrong_keyring_leaf.signature =
874 keys::sign_hex(&owner, &wrong_keyring_leaf.canonical_bytes()).1;
875 let bytes = keys::seal_box_encrypt(
876 &serde_json::to_vec(&wrong_keyring_leaf).expect("serialize wrong-keyring leaf"),
877 &recipient_key,
878 );
879 let wrong_keyring_leaf = PreparedAccessLeaf {
880 leaf_hash: ObjectHash::digest(&bytes),
881 bytes,
882 value: wrong_keyring_leaf,
883 };
884 assert!(!wrong_keyring_leaf.verify(&creation.control, candidate_family));
885
886 let mut wrong_policy_control = creation.control.value.clone();
887 wrong_policy_control.store_membership =
888 serial_membership_ref(&owner, &members, "wrong-policy-control");
889 wrong_policy_control.membership_authority = None;
890 wrong_policy_control.signature =
891 keys::sign_hex(&owner, &wrong_policy_control.canonical_bytes()).1;
892 assert!(!wrong_policy_control.verify());
893 }
894
895 #[test]
896 fn circle_id_round_trips_only_its_canonical_lowercase_base32() {
897 let id = CircleId::from_bytes([0; 16]);
898 let encoded = id.to_string();
899 assert_eq!(encoded.len(), CIRCLE_ID_LENGTH);
900 assert_eq!(encoded.parse::<CircleId>().unwrap(), id);
901 assert!(encoded.to_uppercase().parse::<CircleId>().is_err());
902 assert!("local".parse::<CircleId>().is_err());
903 assert!(format!("{}b", &encoded[..25]).parse::<CircleId>().is_err());
904 }
905
906 #[test]
907 fn recipient_slot_rejects_the_ed25519_identity_point() {
908 let local = UserKeypair::generate();
909 let mut identity = [0; keys::SIGN_PUBLICKEYBYTES];
910 identity[0] = 1;
911 let recipient = hex::encode(identity);
912
913 assert_eq!(
914 recipient_slot_with_peer(&local, &recipient, CircleId::from_bytes([9; 16])),
915 Err(CircleCreateError::InvalidRecipient(recipient))
916 );
917 }
918
919 #[test]
920 fn row_routing_id_is_stable_across_store_key_rotation() {
921 let root = ObjectHash::digest(b"store-root");
922 let before = EncryptionService::from_key([1u8; 32]);
923 let after = before
924 .with_appended_generation(2, [2u8; 32])
925 .expect("append generation");
926 let before_id = row_routing_id(
927 &derive_row_routing_key(&before, root).unwrap(),
928 "accounts",
929 "row-1",
930 );
931 let after_id = row_routing_id(
932 &derive_row_routing_key(&after, root).unwrap(),
933 "accounts",
934 "row-1",
935 );
936 assert_eq!(before_id, after_id);
937 assert_ne!(
938 before_id,
939 row_routing_id(
940 &derive_row_routing_key(&after, root).unwrap(),
941 "accounts",
942 "row-2",
943 )
944 );
945 }
946
947 #[test]
948 fn row_routing_key_requires_exactly_one_generation_one_key() {
949 let root = ObjectHash::digest(b"store-root");
950 let missing = EncryptionService::from_key_at_generation(2, [2u8; 32]);
951 assert!(matches!(
952 derive_row_routing_key(&missing, root),
953 Err(RowRoutingKeyError::MissingGenerationOne)
954 ));
955
956 let ambiguous = EncryptionService::from_keyring([(1, [1u8; 32]), (1, [2u8; 32])])
957 .expect("build forked generation one");
958 assert!(matches!(
959 derive_row_routing_key(&ambiguous, root),
960 Err(RowRoutingKeyError::AmbiguousGenerationOne)
961 ));
962 }
963
964 #[tokio::test]
965 async fn control_history_caches_the_verified_access_owner_and_rejects_second_genesis() {
966 let author = crate::keys::UserKeypair::generate();
967 let author_pubkey = crate::keys::public_key_hex(&author);
968 let earlier_owner = loop {
969 let candidate = crate::keys::UserKeypair::generate();
970 if crate::keys::public_key_hex(&candidate) < author_pubkey {
971 break candidate;
972 }
973 };
974 let earlier_owner_pubkey = crate::keys::public_key_hex(&earlier_owner);
975 let members = vec![
976 (
977 author_pubkey.clone(),
978 super::super::membership::MemberRole::Owner,
979 ),
980 (
981 earlier_owner_pubkey.clone(),
982 super::super::membership::MemberRole::Owner,
983 ),
984 ];
985 let store_root_hash = ObjectHash::digest(b"multi-owner-store-root");
986 let (membership, membership_authority) =
987 merge_membership_ref(&author, &members, "multi-owner-control");
988 let device = merge_device_authority(&author, store_root_hash, "multi-owner-device");
989 let ids = crate::id_provider::SequentialIdProvider::new("multi-owner-control");
990 let operation_id = crate::WriteId::from_generated("multi-owner-control-commit".to_string());
991 let order = super::super::store_commit::StoreCommitOrder::MergeConcurrent {
992 seq: 1,
993 predecessor: None,
994 dependencies: BTreeMap::new(),
995 };
996 let candidate_family = super::super::store_commit::CandidateFamilyId::derive(
997 store_root_hash,
998 &device.reference,
999 &operation_id,
1000 &order,
1001 );
1002 let creation = CircleCreation::founder(
1003 store_root_hash,
1004 candidate_family,
1005 &device.reference.device_id.to_string(),
1006 "Household",
1007 "0000000001000-0000-device-a",
1008 membership.clone(),
1009 Some(membership_authority.clone()),
1010 members,
1011 &ids,
1012 &author,
1013 )
1014 .expect("construct founder circle");
1015 let mut control = creation.control.value.clone();
1016 control.owners = vec![earlier_owner_pubkey, author_pubkey.clone()];
1017 control.owners.sort();
1018 assert_ne!(control.owners[0], control.author_pubkey);
1019 control.signature = keys::sign_hex(&author, &control.canonical_bytes()).1;
1020 let control = PreparedCircleControl {
1021 coord: control.coord(),
1022 bytes: serde_json::to_vec(&control).expect("serialize control"),
1023 value: control,
1024 };
1025 let reference = super::super::store_commit::CircleControlRef::MergeConcurrent {
1026 circle_id: creation.circle_id,
1027 control: control.coord.clone(),
1028 head_hash: ObjectHash::digest(b"multi-owner-control-head"),
1029 objects: test_activation_objects("multi-owner"),
1030 };
1031 let first_coord = super::super::store_commit::StoreCommitCoord::MergeConcurrent {
1032 stream_id: device.stream_id,
1033 sequence: 1,
1034 };
1035 let commit = super::super::store_commit::StoreBatchCommit::signed_operations(
1036 store_root_hash,
1037 operation_id,
1038 first_coord.clone(),
1039 device.reference.clone(),
1040 &device.registration,
1041 order,
1042 membership.clone(),
1043 super::super::store_commit::StoreDeviceStateRef::MergeConcurrent {
1044 frontier: super::super::store_commit::CommitFrontier::MergeConcurrent(
1045 BTreeMap::new(),
1046 ),
1047 recovery: Vec::new(),
1048 state_hash: ObjectHash::digest(b"multi-owner initial device state"),
1049 },
1050 Some(membership_authority.clone()),
1051 super::super::store_commit::StoreCommitOperationsInput {
1052 control: None,
1053 device_join_attempts: Vec::new(),
1054 device_join_outcomes: Vec::new(),
1055 device_join_abandonments: Vec::new(),
1056 device_join_cleanup_receipts: Vec::new(),
1057 provider_access_grants: Vec::new(),
1058 provider_access_withdrawals: Vec::new(),
1059 device_registrations: Vec::new(),
1060 circle_controls: vec![reference.clone()],
1061 store_package: None,
1062 circle_packages: &[],
1063 },
1064 &device.device_signer,
1065 )
1066 .expect("sign Store commit");
1067 let first_commit_path = format!(
1068 "{}.json",
1069 super::super::store_commit::commit_semantic_prefix(
1070 commit.candidate_family(),
1071 &device.stream_id.to_string(),
1072 1,
1073 commit.commit_hash(),
1074 )
1075 );
1076 let commit_ref = super::super::store_commit::StoreBatchCommitRef::from_commit(
1077 &commit,
1078 first_coord,
1079 exact_logical_object(first_commit_path, &commit.to_bytes()),
1080 )
1081 .expect("reference first Store commit");
1082 let own_access = creation
1083 .access
1084 .iter()
1085 .find(|access| access.leaf.value.recipient_pubkey == author_pubkey)
1086 .expect("author access");
1087 let verified = super::super::circle_ops::VerifiedCircleReference {
1088 reference,
1089 circle_id: creation.circle_id,
1090 control: control.clone(),
1091 local_access: Some(super::super::circle_ops::VerifiedCircleAccess {
1092 leaf: own_access.leaf.clone(),
1093 active: Some(super::super::circle_ops::VerifiedCircleActive {
1094 roster: creation.resolved_roster(),
1095 metadata: creation.metadata.clone(),
1096 }),
1097 }),
1098 };
1099 let db = super::super::test_helpers::open_test_db();
1100 let first_commit = commit.clone();
1101 let first_commit_ref = commit_ref.clone();
1102 db.call(move |conn| {
1103 crate::database::Database::record_verified_circle_activations_on(
1104 conn,
1105 &first_commit,
1106 &first_commit_ref,
1107 &[verified],
1108 )
1109 })
1110 .await
1111 .expect("record multi-Owner control");
1112 let circle_id = creation.circle_id.to_string();
1113 let cached_owner = db
1114 .call(move |conn| {
1115 conn.query_row(
1116 "SELECT owner_pubkey FROM circle_access_cache WHERE circle_id = ?1",
1117 [circle_id],
1118 |row| row.get::<_, String>(0),
1119 )
1120 .map_err(crate::database::DbError::from)
1121 })
1122 .await
1123 .expect("read cached access owner");
1124 assert_eq!(cached_owner, author_pubkey);
1125
1126 let mut second_value = control.value.clone();
1127 second_value.access_root = ObjectHash::digest(b"different founder access root");
1128 second_value.signature = keys::sign_hex(&author, &second_value.canonical_bytes()).1;
1129 let second_control = PreparedCircleControl {
1130 coord: second_value.coord(),
1131 bytes: serde_json::to_vec(&second_value).expect("serialize second founder control"),
1132 value: second_value,
1133 };
1134 let second_reference = super::super::store_commit::CircleControlRef::MergeConcurrent {
1135 circle_id: creation.circle_id,
1136 control: second_control.coord.clone(),
1137 head_hash: ObjectHash::digest(b"second-founder-control-head"),
1138 objects: test_activation_objects("second-founder"),
1139 };
1140 let second_coord = super::super::store_commit::StoreCommitCoord::MergeConcurrent {
1141 stream_id: device.stream_id,
1142 sequence: 2,
1143 };
1144 let second_commit = super::super::store_commit::StoreBatchCommit::signed_operations(
1145 store_root_hash,
1146 crate::WriteId::from_generated("second-founder-control-commit".to_string()),
1147 second_coord.clone(),
1148 device.reference,
1149 &device.registration,
1150 super::super::store_commit::StoreCommitOrder::MergeConcurrent {
1151 seq: 2,
1152 predecessor: Some(commit_ref.clone()),
1153 dependencies: BTreeMap::new(),
1154 },
1155 membership,
1156 super::super::store_commit::StoreDeviceStateRef::MergeConcurrent {
1157 frontier: super::super::store_commit::CommitFrontier::MergeConcurrent(
1158 BTreeMap::from([(device.stream_id, commit_ref.clone())]),
1159 ),
1160 recovery: Vec::new(),
1161 state_hash: ObjectHash::digest(b"multi-owner second device state"),
1162 },
1163 control.value.membership_authority.clone(),
1164 super::super::store_commit::StoreCommitOperationsInput {
1165 control: None,
1166 device_join_attempts: Vec::new(),
1167 device_join_outcomes: Vec::new(),
1168 device_join_abandonments: Vec::new(),
1169 device_join_cleanup_receipts: Vec::new(),
1170 provider_access_grants: Vec::new(),
1171 provider_access_withdrawals: Vec::new(),
1172 device_registrations: Vec::new(),
1173 circle_controls: vec![second_reference.clone()],
1174 store_package: None,
1175 circle_packages: &[],
1176 },
1177 &device.device_signer,
1178 )
1179 .expect("sign second founder Store commit");
1180 let second_commit_path = format!(
1181 "{}.json",
1182 super::super::store_commit::commit_semantic_prefix(
1183 second_commit.candidate_family(),
1184 &device.stream_id.to_string(),
1185 2,
1186 second_commit.commit_hash(),
1187 )
1188 );
1189 let second_commit_ref = super::super::store_commit::StoreBatchCommitRef::from_commit(
1190 &second_commit,
1191 second_coord,
1192 exact_logical_object(second_commit_path, &second_commit.to_bytes()),
1193 )
1194 .expect("reference second Store commit");
1195 let error = db
1196 .call(move |conn| {
1197 crate::database::Database::record_verified_circle_activations_on(
1198 conn,
1199 &second_commit,
1200 &second_commit_ref,
1201 &[super::super::circle_ops::VerifiedCircleReference {
1202 reference: second_reference,
1203 circle_id: creation.circle_id,
1204 control: second_control,
1205 local_access: None,
1206 }],
1207 )
1208 })
1209 .await
1210 .expect_err("a Circle cannot accept a second founder control");
1211 assert!(
1212 error.to_string().contains("already has a founder"),
1213 "{error}"
1214 );
1215 }
1216}