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 key_fingerprint: KeyFingerprint,
123 writers: BTreeSet<String>,
124}
125
126pub(super) struct VerifiedCircleKeyring {
127 keyring: EncryptionService,
128 key_fingerprint: KeyFingerprint,
129}
130
131impl VerifiedCircleKeyring {
132 fn key_entry(&self, fingerprint: KeyFingerprint) -> Option<(u64, [u8; 32])> {
133 self.keyring
134 .keyring_entries()
135 .into_iter()
136 .find(|(generation, key)| {
137 EncryptionService::from_key_at_generation(*generation, *key).seal_key_fingerprint()
138 == fingerprint
139 })
140 }
141}
142
143impl CircleEpochAccess {
144 pub fn key_fingerprint(&self) -> KeyFingerprint {
145 self.key_fingerprint
146 }
147
148 pub fn protocol_context(
149 &self,
150 store_root_hash: ObjectHash,
151 domain: crate::objects::CircleProtocolObjectDomain,
152 ) -> crate::objects::ProtocolObjectContext {
153 crate::objects::ProtocolObjectContext::circle(
154 store_root_hash,
155 domain,
156 self.encryption.clone(),
157 )
158 }
159
160 pub fn blob_protection(&self) -> crate::objects::BlobSpoolProtection {
161 crate::objects::BlobSpoolProtection::Opaque(self.encryption.clone())
162 }
163
164 pub fn from_historical(
165 circle_id: CircleId,
166 key_fingerprint: KeyFingerprint,
167 serialized_keyring: &str,
168 roster: &CircleMaterializedRoster,
169 ) -> Result<Self, CircleStateError> {
170 if !roster.verify() {
171 return Err(CircleStateError::Invariant(format!(
172 "Circle {circle_id} historical package roster is invalid"
173 )));
174 }
175 let keyring = MasterKeyring::from_serialized(serialized_keyring).map_err(|source| {
176 CircleStateError::Encryption {
177 operation: "parse historical package keyring",
178 circle_id,
179 source,
180 }
181 })?;
182 let encryption = EncryptionService::from(keyring)
183 .service_for_fingerprint(key_fingerprint.as_bytes())
184 .map_err(|source| CircleStateError::Encryption {
185 operation: "select historical package key",
186 circle_id,
187 source,
188 })?;
189 Ok(Self {
190 circle_id,
191 encryption,
192 key_fingerprint,
193 writers: roster.members().keys().cloned().collect(),
194 })
195 }
196
197 pub fn authorize_package(
198 &self,
199 reference: &CirclePackageRef,
200 author: &StoreDeviceRegistration,
201 ) -> Result<(), CircleStateError> {
202 if reference.circle_id != self.circle_id {
203 return Err(CircleStateError::Invariant(format!(
204 "Circle package names {}, but access belongs to {}",
205 reference.circle_id, self.circle_id
206 )));
207 }
208 if !self.writers.contains(&author.author_pubkey) {
209 return Err(CircleStateError::Invariant(format!(
210 "Circle package author is not a member of {} at its exact control",
211 reference.circle_id
212 )));
213 }
214 if self.key_fingerprint != reference.key_fingerprint {
215 return Err(CircleStateError::Invariant(format!(
216 "Circle package key for {} differs from its activated control",
217 reference.circle_id
218 )));
219 }
220 Ok(())
221 }
222
223 #[cfg(any(test, feature = "test-utils"))]
224 pub fn authorizes_writer(&self, author_pubkey: &str) -> bool {
225 self.writers.contains(author_pubkey)
226 }
227}
228
229impl VerifiedCircleReference {
230 pub fn retained_key_entry(
231 &self,
232 fingerprint: KeyFingerprint,
233 ) -> Result<Option<(u64, [u8; 32])>, CircleStateError> {
234 let Some(access) = self.local_access.as_ref() else {
235 return Ok(None);
236 };
237 let Some(active) = access.active.as_ref() else {
238 return Ok(None);
239 };
240 verified_keyring_from(
241 self.circle_id,
242 &self.control.value,
243 &access.leaf.value.disposition,
244 &active.roster,
245 )
246 .map(|keyring| keyring.key_entry(fingerprint))
247 }
248
249 pub fn epoch_access(&self) -> Result<Option<CircleEpochAccess>, CircleStateError> {
250 let Some(access) = self.local_access.as_ref() else {
251 return Ok(None);
252 };
253 let Some(active) = access.active.as_ref() else {
254 return Ok(None);
255 };
256 epoch_access_from(
257 self.circle_id,
258 &self.control.value,
259 &access.leaf.value.disposition,
260 &active.roster,
261 )
262 .map(Some)
263 }
264}
265
266pub(super) fn epoch_access_from(
267 circle_id: CircleId,
268 control: &CircleControl,
269 disposition: &CircleAccessDisposition,
270 roster: &CircleMaterializedRoster,
271) -> Result<CircleEpochAccess, CircleStateError> {
272 let verified = verified_keyring_from(circle_id, control, disposition, roster)?;
273 let encryption = verified
274 .keyring
275 .service_for_fingerprint(verified.key_fingerprint.as_bytes())
276 .map_err(|source| CircleStateError::Encryption {
277 operation: "select package key",
278 circle_id,
279 source,
280 })?;
281 let key_fingerprint = verified.key_fingerprint;
282 Ok(CircleEpochAccess {
283 circle_id,
284 encryption,
285 key_fingerprint,
286 writers: roster.members().keys().cloned().collect(),
287 })
288}
289
290pub(super) fn verified_keyring_from(
291 circle_id: CircleId,
292 control: &CircleControl,
293 disposition: &CircleAccessDisposition,
294 roster: &CircleMaterializedRoster,
295) -> Result<VerifiedCircleKeyring, CircleStateError> {
296 if control.circle_id != circle_id
297 || !roster.verify()
298 || roster.state_hash() != control.roster_state_ref().state_hash
299 {
300 return Err(CircleStateError::Invariant(format!(
301 "Circle {circle_id} package roster differs from its activated control"
302 )));
303 }
304 let CircleAccessDisposition::Active {
305 keyring,
306 key_fingerprint,
307 ..
308 } = disposition
309 else {
310 return Err(CircleStateError::Invariant(format!(
311 "active Circle access for {circle_id} has an inactive leaf"
312 )));
313 };
314 if *key_fingerprint != control.key_fingerprint() {
315 return Err(CircleStateError::Invariant(format!(
316 "Circle package key for {circle_id} differs from its activated control"
317 )));
318 }
319 let keyring =
320 MasterKeyring::from_serialized(keyring).map_err(|source| CircleStateError::Encryption {
321 operation: "parse package keyring",
322 circle_id,
323 source,
324 })?;
325 let keyring = EncryptionService::from(keyring);
326 keyring
327 .service_for_fingerprint(key_fingerprint.as_bytes())
328 .map_err(|source| CircleStateError::Encryption {
329 operation: "select package key",
330 circle_id,
331 source,
332 })?;
333 Ok(VerifiedCircleKeyring {
334 keyring,
335 key_fingerprint: *key_fingerprint,
336 })
337}