coven_protocol/circle_activation/
access.rs1use super::*;
2
3#[derive(Debug, Clone, PartialEq, Eq)]
4pub struct LocalCircleExclusion {
5 pub circle_id: CircleId,
6 pub close_id: CircleEpochCloseId,
7 pub excluded: StoreDeviceRegistrationRef,
8 pub successor_control: CircleControlCoord,
9 pub activating_commit: StoreBatchCommitRef,
10}
11
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct VerifiedCircleReference {
14 pub reference: CircleControlRef,
15 pub circle_id: CircleId,
16 pub control: PreparedCircleControl,
17 pub local_access: Option<VerifiedCircleAccess>,
18}
19
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct VerifiedCircleAccess {
22 pub envelope: AccessEnvelope,
23 pub leaf: PreparedAccessLeaf,
24 pub active: Option<VerifiedCircleActive>,
25}
26
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct VerifiedCircleActive {
29 pub roster: CircleMaterializedRoster,
30 pub metadata: CircleMetadata,
31}
32
33#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
34#[serde(deny_unknown_fields)]
35pub struct VerifiedCircleImage {
36 pub(super) circle_id: CircleId,
37 pub(super) control: CircleControlCoord,
38 pub(super) reference: CircleBootstrapRef,
39 pub(super) image_bytes: Vec<u8>,
40}
41
42impl VerifiedCircleImage {
43 pub fn new(
44 circle_id: CircleId,
45 control: CircleControlCoord,
46 access: &CircleAccessLeaf,
47 reference: CircleBootstrapRef,
48 image_bytes: Vec<u8>,
49 ) -> Result<Self, CircleStateError> {
50 let verified = Self {
51 circle_id,
52 control,
53 reference,
54 image_bytes,
55 };
56 verified.verify_for_access(access)?;
57 Ok(verified)
58 }
59
60 pub fn from_stored_image(
68 circle_id: CircleId,
69 control: CircleControlCoord,
70 reference: CircleBootstrapRef,
71 image_bytes: Vec<u8>,
72 ) -> Result<Self, CircleStateError> {
73 if reference.image.image_hash != ObjectHash::digest(&image_bytes) {
74 return Err(CircleStateError::Invariant(
75 "stored Circle image differs from its exact image hash".to_string(),
76 ));
77 }
78 Ok(Self {
79 circle_id,
80 control,
81 reference,
82 image_bytes,
83 })
84 }
85
86 pub(super) fn verify_for_access(
87 &self,
88 access: &CircleAccessLeaf,
89 ) -> Result<(), CircleStateError> {
90 if self.circle_id != access.circle_id
91 || !self.reference.verify_for_access(access)
92 || self.reference.image.image_hash != ObjectHash::digest(&self.image_bytes)
93 {
94 return Err(CircleStateError::Invariant(
95 "verified Circle bootstrap differs from its signed access leaf".to_string(),
96 ));
97 }
98 Ok(())
99 }
100
101 pub fn circle_id(&self) -> CircleId {
102 self.circle_id
103 }
104
105 pub fn control(&self) -> &CircleControlCoord {
106 &self.control
107 }
108
109 pub fn reference(&self) -> &CircleBootstrapRef {
110 &self.reference
111 }
112
113 pub fn image_bytes(&self) -> &[u8] {
114 &self.image_bytes
115 }
116}
117
118#[derive(Clone)]
119pub struct CircleEpochAccess {
120 circle_id: CircleId,
121 encryption: EncryptionService,
122 writers: BTreeSet<String>,
123}
124
125pub(super) struct VerifiedCircleKeyring {
126 keyring: EncryptionService,
127 key_fingerprint: KeyFingerprint,
128}
129
130impl VerifiedCircleKeyring {
131 fn key_entry(&self, fingerprint: KeyFingerprint) -> Option<(u64, [u8; 32])> {
132 self.keyring
133 .keyring_entries()
134 .into_iter()
135 .find(|(generation, key)| {
136 EncryptionService::from_key_at_generation(*generation, *key).seal_key_fingerprint()
137 == fingerprint
138 })
139 }
140}
141
142impl CircleEpochAccess {
143 pub fn circle_id(&self) -> CircleId {
144 self.circle_id
145 }
146
147 pub fn key_fingerprint(&self) -> KeyFingerprint {
148 self.encryption.seal_key_fingerprint()
149 }
150
151 pub fn protocol_context(
152 &self,
153 store_root_hash: ObjectHash,
154 domain: crate::objects::CircleProtocolObjectDomain,
155 ) -> crate::objects::ProtocolObjectContext {
156 crate::objects::ProtocolObjectContext::circle(
157 store_root_hash,
158 domain,
159 self.encryption.clone(),
160 )
161 }
162
163 pub fn blob_protection(&self) -> crate::objects::BlobSpoolProtection {
164 crate::objects::BlobSpoolProtection::Opaque(self.encryption.clone())
165 }
166
167 pub fn from_historical(
168 circle_id: CircleId,
169 key_fingerprint: KeyFingerprint,
170 serialized_keyring: &str,
171 roster: &CircleMaterializedRoster,
172 ) -> Result<Self, CircleStateError> {
173 if !roster.verify() {
174 return Err(CircleStateError::Invariant(format!(
175 "Circle {circle_id} historical package roster is invalid"
176 )));
177 }
178 let keyring = MasterKeyring::from_serialized(serialized_keyring).map_err(|source| {
179 CircleStateError::Encryption {
180 operation: "parse historical package keyring",
181 circle_id,
182 source,
183 }
184 })?;
185 let encryption = EncryptionService::from(keyring)
186 .service_for_fingerprint(key_fingerprint.as_bytes())
187 .map_err(|source| CircleStateError::Encryption {
188 operation: "select historical package key",
189 circle_id,
190 source,
191 })?;
192 Ok(Self {
193 circle_id,
194 encryption,
195 writers: roster.members().keys().cloned().collect(),
196 })
197 }
198
199 pub fn authorize_package(
200 &self,
201 reference: &CirclePackageRef,
202 author: &StoreDeviceRegistration,
203 ) -> Result<(), CircleStateError> {
204 if reference.circle_id != self.circle_id {
205 return Err(CircleStateError::Invariant(format!(
206 "Circle package names {}, but access belongs to {}",
207 reference.circle_id, self.circle_id
208 )));
209 }
210 if !self.writers.contains(&author.author_pubkey) {
211 return Err(CircleStateError::Invariant(format!(
212 "Circle package author is not a member of {} at its exact control",
213 reference.circle_id
214 )));
215 }
216 if self.key_fingerprint() != reference.key_fingerprint {
217 return Err(CircleStateError::Invariant(format!(
218 "Circle package key for {} differs from its activated control",
219 reference.circle_id
220 )));
221 }
222 Ok(())
223 }
224
225 #[cfg(any(test, feature = "test-utils"))]
226 pub fn authorizes_writer(&self, author_pubkey: &str) -> bool {
227 self.writers.contains(author_pubkey)
228 }
229}
230
231impl VerifiedCircleReference {
232 pub fn retained_key_entry(
233 &self,
234 fingerprint: KeyFingerprint,
235 ) -> Result<Option<(u64, [u8; 32])>, CircleStateError> {
236 let Some(access) = self.local_access.as_ref() else {
237 return Ok(None);
238 };
239 let Some(active) = access.active.as_ref() else {
240 return Ok(None);
241 };
242 verified_keyring_from(
243 self.circle_id,
244 &self.control.value,
245 &access.leaf.value.disposition,
246 &active.roster,
247 )
248 .map(|keyring| keyring.key_entry(fingerprint))
249 }
250
251 pub fn snapshot_keyring(&self) -> Result<Option<EncryptionService>, CircleStateError> {
254 let Some(access) = self.local_access.as_ref() else {
255 return Ok(None);
256 };
257 let Some(active) = access.active.as_ref() else {
258 return Ok(None);
259 };
260 verified_keyring_from(
261 self.circle_id,
262 &self.control.value,
263 &access.leaf.value.disposition,
264 &active.roster,
265 )
266 .map(|verified| Some(verified.keyring))
267 }
268
269 pub fn epoch_access(&self) -> Result<Option<CircleEpochAccess>, CircleStateError> {
270 let Some(access) = self.local_access.as_ref() else {
271 return Ok(None);
272 };
273 let Some(active) = access.active.as_ref() else {
274 return Ok(None);
275 };
276 epoch_access_from(
277 self.circle_id,
278 &self.control.value,
279 &access.leaf.value.disposition,
280 &active.roster,
281 )
282 .map(Some)
283 }
284}
285
286pub(super) fn epoch_access_from(
287 circle_id: CircleId,
288 control: &CircleControl,
289 disposition: &CircleAccessDisposition,
290 roster: &CircleMaterializedRoster,
291) -> Result<CircleEpochAccess, CircleStateError> {
292 let verified = verified_keyring_from(circle_id, control, disposition, roster)?;
293 let encryption = verified
294 .keyring
295 .service_for_fingerprint(verified.key_fingerprint.as_bytes())
296 .map_err(|source| CircleStateError::Encryption {
297 operation: "select package key",
298 circle_id,
299 source,
300 })?;
301 Ok(CircleEpochAccess {
302 circle_id,
303 encryption,
304 writers: roster.members().keys().cloned().collect(),
305 })
306}
307
308pub(super) fn verified_keyring_from(
309 circle_id: CircleId,
310 control: &CircleControl,
311 disposition: &CircleAccessDisposition,
312 roster: &CircleMaterializedRoster,
313) -> Result<VerifiedCircleKeyring, CircleStateError> {
314 if control.circle_id != circle_id
315 || !roster.verify()
316 || roster.state_hash() != control.roster_state_ref().state_hash
317 {
318 return Err(CircleStateError::Invariant(format!(
319 "Circle {circle_id} package roster differs from its activated control"
320 )));
321 }
322 let CircleAccessDisposition::Active {
323 keyring,
324 key_fingerprint,
325 ..
326 } = disposition
327 else {
328 return Err(CircleStateError::Invariant(format!(
329 "active Circle access for {circle_id} has an inactive leaf"
330 )));
331 };
332 if *key_fingerprint != control.key_fingerprint() {
333 return Err(CircleStateError::Invariant(format!(
334 "Circle package key for {circle_id} differs from its activated control"
335 )));
336 }
337 let keyring =
338 MasterKeyring::from_serialized(keyring).map_err(|source| CircleStateError::Encryption {
339 operation: "parse package keyring",
340 circle_id,
341 source,
342 })?;
343 let keyring = EncryptionService::from(keyring);
344 keyring
345 .service_for_fingerprint(key_fingerprint.as_bytes())
346 .map_err(|source| CircleStateError::Encryption {
347 operation: "select package key",
348 circle_id,
349 source,
350 })?;
351 Ok(VerifiedCircleKeyring {
352 keyring,
353 key_fingerprint: *key_fingerprint,
354 })
355}