Skip to main content

coven_protocol/circle_activation/
access.rs

1use 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    /// Reconstruct a verified Circle image from stored bytes and an exact
61    /// reference — the coverage-row and restore-selection path, which has no
62    /// access leaf (a standalone snapshot names none). The bytes are input to the
63    /// verifier, never trusted for being local: their digest must equal the
64    /// reference's image hash, and the caller separately runs
65    /// `verify_circle_bootstrap_image` against the retained control and routing
66    /// key for the full schema/routing/audience/blob-closure check.
67    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    /// Snapshot streams can contain images from earlier epochs retained in the
252    /// recipient's signed keyring. Package access still selects its exact key.
253    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}