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    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}