Skip to main content

coven_protocol/store_commit/
registration.rs

1use super::*;
2
3#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4#[serde(deny_unknown_fields)]
5pub struct DeviceRecoveryReadiness {
6    pub registration: StoreDeviceRegistrationRef,
7    pub initial_ack: StoreAckRef,
8    pub bootstrap_cut: StoreHistoryCut,
9}
10
11#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
12#[serde(deny_unknown_fields)]
13pub struct OwnerRecoveryNodeBody {
14    pub store_root_hash: ObjectHash,
15    pub recovery_id: DeviceRecoveryId,
16    pub owner_pubkey: String,
17    pub owner_grant: MembershipGrantId,
18    pub sequence: u64,
19    pub membership: StoreMembershipStateRef,
20    pub predecessor: Option<OwnerRecoveryNodeRef>,
21    pub readiness: DeviceRecoveryReadiness,
22    pub next_slot: ObjectSlot,
23}
24
25impl SignedBody for OwnerRecoveryNodeBody {
26    const DOMAIN: &'static [u8] = OWNER_RECOVERY_NODE_DOMAIN;
27}
28
29pub type OwnerRecoveryNode = Signed<OwnerRecoveryNodeBody>;
30
31impl OwnerRecoveryNode {
32    #[allow(clippy::too_many_arguments)]
33    pub fn signed(
34        store_root_hash: ObjectHash,
35        recovery_id: DeviceRecoveryId,
36        owner_grant: MembershipGrantId,
37        sequence: u64,
38        membership: StoreMembershipStateRef,
39        predecessor: Option<OwnerRecoveryNodeRef>,
40        readiness: DeviceRecoveryReadiness,
41        next_slot: ObjectSlot,
42        owner_signer: &UserKeypair,
43    ) -> Result<Self, StoreProtocolError> {
44        let body = OwnerRecoveryNodeBody {
45            store_root_hash,
46            recovery_id,
47            owner_pubkey: keys::public_key_hex(owner_signer),
48            owner_grant,
49            sequence,
50            membership,
51            predecessor,
52            readiness,
53            next_slot,
54        };
55        body.validate_shape()?;
56        Ok(Signed::sign(body, owner_signer))
57    }
58
59    pub fn parse_at(
60        bytes: &[u8],
61        store_root: &StoreRootRef,
62        reference: &OwnerRecoveryNodeRef,
63    ) -> Result<Self, StoreProtocolError> {
64        let node: Self = crate::objects::decode_protocol_object(bytes)?;
65        node.body().validate_shape()?;
66        if node.store_root_hash != store_root.store_root_hash
67            || node.owner_pubkey != reference.owner_pubkey
68            || node.owner_grant != reference.owner_grant
69            || node.sequence != reference.sequence
70            || node.node_hash() != reference.node_hash
71        {
72            return Err(StoreProtocolError::OwnerRecoveryMismatch);
73        }
74        let owner_pubkey = node.owner_pubkey.clone();
75        node.verify_by(&owner_pubkey)?;
76        Ok(node)
77    }
78
79    pub fn node_hash(&self) -> ObjectHash {
80        self.hash()
81    }
82}
83
84impl OwnerRecoveryNodeBody {
85    fn validate_shape(&self) -> Result<(), StoreProtocolError> {
86        let predecessor_matches = match &self.predecessor {
87            None => self.sequence == 1,
88            Some(predecessor) => {
89                predecessor.owner_pubkey == self.owner_pubkey
90                    && predecessor.owner_grant == self.owner_grant
91                    && predecessor.sequence.checked_add(1) == Some(self.sequence)
92            }
93        };
94        if !predecessor_matches || self.readiness.initial_ack.sequence != 1 {
95            return Err(StoreProtocolError::OwnerRecoveryMismatch);
96        }
97        Ok(())
98    }
99}
100
101#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
102#[serde(rename_all = "snake_case", deny_unknown_fields)]
103pub enum StoreDeviceRegistrationOrigin {
104    Founder {
105        creation_id: StoreCreationId,
106    },
107    Join {
108        attempt_id: DeviceJoinAttemptId,
109    },
110    Recovery {
111        recovery_id: DeviceRecoveryId,
112        recovery_slot: ObjectSlot,
113        owner_grant: MembershipGrantId,
114    },
115}
116
117#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
118#[serde(rename_all = "snake_case", deny_unknown_fields)]
119pub enum StoreDeviceRegistrationActivation {
120    Founder {
121        root: StoreRootRef,
122    },
123    Join {
124        attempt_id: DeviceJoinAttemptId,
125    },
126    Recovery {
127        recovery_id: DeviceRecoveryId,
128        node: OwnerRecoveryNodeRef,
129    },
130}
131
132#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
133#[serde(deny_unknown_fields)]
134pub struct ActivatedStoreDeviceRegistrationRef {
135    pub registration: StoreDeviceRegistrationRef,
136    pub authority: StoreDeviceRegistrationActivationRef,
137}
138
139#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
140#[serde(rename_all = "snake_case", deny_unknown_fields)]
141pub enum StoreDeviceRegistrationActivationRef {
142    Join {
143        attempt_id: DeviceJoinAttemptId,
144    },
145    Recovery {
146        recovery_id: DeviceRecoveryId,
147        node: OwnerRecoveryNodeRef,
148    },
149}
150
151impl StoreDeviceRegistrationOrigin {
152    pub(super) fn external_id(&self) -> ObjectHash {
153        match self {
154            Self::Founder { creation_id } => creation_id.object_hash(),
155            Self::Join { attempt_id, .. } => attempt_id.object_hash(),
156            Self::Recovery { recovery_id, .. } => recovery_id.object_hash(),
157        }
158    }
159}
160
161#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
162#[serde(rename_all = "snake_case", deny_unknown_fields)]
163pub enum DeviceStreamAnchor {
164    StoreAcknowledgements {
165        first_slot: ObjectSlot,
166    },
167    /// Per-(device, Circle) acknowledgement stream. Unlike the permanent
168    /// Store anchor above, this is never a registration field: it is derived on
169    /// demand to bind one device's Circle-acknowledgement stream to its Circle.
170    CircleAcknowledgements {
171        circle_id: CircleId,
172        first_slot: ObjectSlot,
173    },
174    /// Per-(device, Circle) snapshot stream. Like the Circle-acknowledgement
175    /// anchor, never a registration field: derived on demand to bind one
176    /// device's Circle-snapshot stream to its Circle.
177    CircleSnapshots {
178        circle_id: CircleId,
179        first_slot: ObjectSlot,
180    },
181}
182
183impl DeviceStreamAnchor {
184    pub fn first_slot(&self) -> &ObjectSlot {
185        match self {
186            Self::StoreAcknowledgements { first_slot }
187            | Self::CircleAcknowledgements { first_slot, .. }
188            | Self::CircleSnapshots { first_slot, .. } => first_slot,
189        }
190    }
191}
192
193#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
194#[serde(rename_all = "snake_case", deny_unknown_fields)]
195pub enum GrantStreamAnchor {
196    StoreMembership {
197        first_slot: ObjectSlot,
198    },
199    OwnerRecovery {
200        first_slot: ObjectSlot,
201    },
202    CircleControl {
203        circle_id: CircleId,
204        first_slot: ObjectSlot,
205    },
206    CircleRoster {
207        circle_id: CircleId,
208        first_slot: ObjectSlot,
209    },
210    CircleMetadata {
211        circle_id: CircleId,
212        first_slot: ObjectSlot,
213    },
214}
215
216impl GrantStreamAnchor {
217    pub fn first_slot(&self) -> &ObjectSlot {
218        match self {
219            Self::StoreMembership { first_slot }
220            | Self::OwnerRecovery { first_slot }
221            | Self::CircleControl { first_slot, .. }
222            | Self::CircleRoster { first_slot, .. }
223            | Self::CircleMetadata { first_slot, .. } => first_slot,
224        }
225    }
226}
227
228#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
229#[serde(deny_unknown_fields)]
230pub struct StoreDeviceRegistrationBody {
231    pub store_root: StoreRootRef,
232    pub device_id: StoreDeviceId,
233    pub author_pubkey: String,
234    pub device_signing_pubkey: String,
235    pub origin: StoreDeviceRegistrationOrigin,
236    pub provider: ProviderDeviceBinding,
237    pub acknowledgements: DeviceStreamAnchor,
238}
239
240impl SignedBody for StoreDeviceRegistrationBody {
241    const DOMAIN: &'static [u8] = REGISTRATION_DOMAIN;
242}
243
244pub type StoreDeviceRegistration = Signed<StoreDeviceRegistrationBody>;
245
246#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
247pub struct ReferencedStoreDeviceRegistration {
248    reference: StoreDeviceRegistrationRef,
249    value: StoreDeviceRegistration,
250}
251
252impl ReferencedStoreDeviceRegistration {
253    pub fn verified(
254        reference: StoreDeviceRegistrationRef,
255        value: StoreDeviceRegistration,
256    ) -> Result<Self, StoreProtocolError> {
257        reference.verify_registration(&value)?;
258        Ok(Self { reference, value })
259    }
260
261    pub fn reference(&self) -> &StoreDeviceRegistrationRef {
262        &self.reference
263    }
264
265    pub fn value(&self) -> &StoreDeviceRegistration {
266        &self.value
267    }
268}
269
270impl<'de> Deserialize<'de> for ReferencedStoreDeviceRegistration {
271    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
272    where
273        D: serde::Deserializer<'de>,
274    {
275        #[derive(Deserialize)]
276        #[serde(deny_unknown_fields)]
277        struct EncodedRegistration {
278            reference: StoreDeviceRegistrationRef,
279            value: StoreDeviceRegistration,
280        }
281
282        let encoded = EncodedRegistration::deserialize(deserializer)?;
283        Self::verified(encoded.reference, encoded.value).map_err(serde::de::Error::custom)
284    }
285}
286
287#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
288pub struct ActivatedStoreDeviceRegistration {
289    registration: ReferencedStoreDeviceRegistration,
290    activation: StoreDeviceRegistrationActivation,
291}
292
293impl ActivatedStoreDeviceRegistration {
294    pub fn verified(
295        registration: ReferencedStoreDeviceRegistration,
296        activation: StoreDeviceRegistrationActivation,
297    ) -> Result<Self, StoreProtocolError> {
298        let value = registration.value();
299        let matches = match (&value.origin, &activation) {
300            (
301                StoreDeviceRegistrationOrigin::Founder { .. },
302                StoreDeviceRegistrationActivation::Founder { root },
303            ) => &value.store_root == root,
304            (
305                StoreDeviceRegistrationOrigin::Join {
306                    attempt_id: origin_attempt,
307                    ..
308                },
309                StoreDeviceRegistrationActivation::Join { attempt_id },
310            ) => origin_attempt == attempt_id,
311            (
312                StoreDeviceRegistrationOrigin::Recovery {
313                    recovery_id: origin_recovery,
314                    recovery_slot,
315                    ..
316                },
317                StoreDeviceRegistrationActivation::Recovery { recovery_id, node },
318            ) => origin_recovery == recovery_id && recovery_slot == node.slot(),
319            _ => false,
320        };
321        if !matches {
322            return Err(StoreProtocolError::DeviceStateMismatch);
323        }
324        Ok(Self {
325            registration,
326            activation,
327        })
328    }
329
330    pub fn verify_reference(
331        &self,
332        reference: &ActivatedStoreDeviceRegistrationRef,
333    ) -> Result<(), StoreProtocolError> {
334        if self.registration.reference() != &reference.registration {
335            return Err(StoreProtocolError::DeviceStateMismatch);
336        }
337        let matches = match (&reference.authority, &self.activation) {
338            (
339                StoreDeviceRegistrationActivationRef::Join { attempt_id },
340                StoreDeviceRegistrationActivation::Join {
341                    attempt_id: activated_attempt,
342                },
343            ) => attempt_id == activated_attempt,
344            (
345                StoreDeviceRegistrationActivationRef::Recovery { recovery_id, node },
346                StoreDeviceRegistrationActivation::Recovery {
347                    recovery_id: activated_recovery,
348                    node: activated_node,
349                },
350            ) => recovery_id == activated_recovery && node == activated_node,
351            _ => false,
352        };
353        if !matches {
354            return Err(StoreProtocolError::DeviceStateMismatch);
355        }
356        Ok(())
357    }
358
359    pub fn activated_reference(
360        &self,
361    ) -> Result<ActivatedStoreDeviceRegistrationRef, StoreProtocolError> {
362        let authority = match &self.activation {
363            StoreDeviceRegistrationActivation::Founder { .. } => {
364                return Err(StoreProtocolError::DeviceStateMismatch)
365            }
366            StoreDeviceRegistrationActivation::Join { attempt_id } => {
367                StoreDeviceRegistrationActivationRef::Join {
368                    attempt_id: *attempt_id,
369                }
370            }
371            StoreDeviceRegistrationActivation::Recovery { recovery_id, node } => {
372                StoreDeviceRegistrationActivationRef::Recovery {
373                    recovery_id: *recovery_id,
374                    node: node.clone(),
375                }
376            }
377        };
378        Ok(ActivatedStoreDeviceRegistrationRef {
379            registration: self.registration.reference().clone(),
380            authority,
381        })
382    }
383
384    pub fn registration(&self) -> &ReferencedStoreDeviceRegistration {
385        &self.registration
386    }
387
388    pub fn reference(&self) -> &StoreDeviceRegistrationRef {
389        self.registration.reference()
390    }
391
392    pub fn value(&self) -> &StoreDeviceRegistration {
393        self.registration.value()
394    }
395
396    pub fn activation(&self) -> &StoreDeviceRegistrationActivation {
397        &self.activation
398    }
399
400    pub(crate) fn recovery_cursor(
401        &self,
402    ) -> Result<Option<OwnerRecoveryCursor>, StoreProtocolError> {
403        match (&self.registration.value().origin, &self.activation) {
404            (
405                StoreDeviceRegistrationOrigin::Recovery {
406                    recovery_id,
407                    recovery_slot,
408                    owner_grant,
409                },
410                StoreDeviceRegistrationActivation::Recovery {
411                    recovery_id: activated_recovery_id,
412                    node,
413                },
414            ) if recovery_id == activated_recovery_id
415                && recovery_slot == node.object.slot()
416                && owner_grant == &node.owner_grant =>
417            {
418                Ok(Some(OwnerRecoveryCursor {
419                    owner_grant: owner_grant.clone(),
420                    position: OwnerRecoveryPosition::At { node: node.clone() },
421                }))
422            }
423            (
424                StoreDeviceRegistrationOrigin::Join { attempt_id, .. },
425                StoreDeviceRegistrationActivation::Join {
426                    attempt_id: activated_attempt_id,
427                },
428            ) if attempt_id == activated_attempt_id => Ok(None),
429            (
430                StoreDeviceRegistrationOrigin::Founder { .. },
431                StoreDeviceRegistrationActivation::Founder { .. },
432            ) => Ok(None),
433            _ => Err(StoreProtocolError::Malformed(
434                "registration origin differs from its exact activation authority".to_string(),
435            )),
436        }
437    }
438}
439
440impl<'de> Deserialize<'de> for ActivatedStoreDeviceRegistration {
441    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
442    where
443        D: serde::Deserializer<'de>,
444    {
445        #[derive(Deserialize)]
446        #[serde(deny_unknown_fields)]
447        struct EncodedActivation {
448            registration: ReferencedStoreDeviceRegistration,
449            activation: StoreDeviceRegistrationActivation,
450        }
451
452        let encoded = EncodedActivation::deserialize(deserializer)?;
453        Self::verified(encoded.registration, encoded.activation).map_err(serde::de::Error::custom)
454    }
455}
456
457impl StoreDeviceRegistration {
458    pub fn store_acknowledgement_activation(
459        &self,
460        reference: &StoreDeviceRegistrationRef,
461    ) -> Result<StreamActivation, StoreProtocolError> {
462        reference.verify_registration(self)?;
463        Ok(StreamActivation::device_authorized(
464            self.store_root.store_root_hash,
465            reference.clone(),
466            self.acknowledgements.clone(),
467        ))
468    }
469
470    pub fn signed(
471        store_root: StoreRootRef,
472        origin: StoreDeviceRegistrationOrigin,
473        provider: ProviderDeviceBinding,
474        acknowledgements: DeviceStreamAnchor,
475        identity_signer: &UserKeypair,
476    ) -> Result<Self, StoreProtocolError> {
477        validate_acknowledgement_anchor(&acknowledgements)?;
478        let author_pubkey = keys::public_key_hex(identity_signer);
479        let device_signer = derive_device_signer(identity_signer, &store_root, &origin);
480        let device_signing_pubkey = keys::public_key_hex(&device_signer);
481        let device_id = StoreDeviceId::derive(&store_root, &origin);
482        Ok(Signed::sign(
483            StoreDeviceRegistrationBody {
484                store_root,
485                device_id,
486                author_pubkey,
487                device_signing_pubkey,
488                origin,
489                provider,
490                acknowledgements,
491            },
492            identity_signer,
493        ))
494    }
495
496    pub fn device_signer(
497        &self,
498        identity_signer: &UserKeypair,
499    ) -> Result<UserKeypair, StoreProtocolError> {
500        if keys::public_key_hex(identity_signer) != self.author_pubkey {
501            return Err(StoreProtocolError::InvalidSignature);
502        }
503        let signer = derive_device_signer(identity_signer, &self.store_root, &self.origin);
504        if keys::public_key_hex(&signer) != self.device_signing_pubkey {
505            return Err(StoreProtocolError::InvalidSignature);
506        }
507        Ok(signer)
508    }
509
510    pub fn registration_hash(&self) -> ObjectHash {
511        self.hash()
512    }
513
514    pub fn parse_at(
515        bytes: &[u8],
516        expected_store_root: &StoreRootRef,
517        expected_device: StoreDeviceId,
518    ) -> Result<Self, StoreProtocolError> {
519        let registration: Self = crate::objects::decode_protocol_object(bytes)?;
520        registration.require_version()?;
521        if &registration.store_root != expected_store_root {
522            return Err(StoreProtocolError::StoreRootMismatch {
523                expected: expected_store_root.store_root_hash,
524                actual: registration.store_root.store_root_hash,
525            });
526        }
527        if registration.device_id != expected_device {
528            return Err(StoreProtocolError::RelocatedSlot {
529                expected: registration_slot_prefix(&expected_device.to_string()),
530                actual: registration_slot_prefix(&registration.device_id.to_string()),
531            });
532        }
533        if registration.device_id
534            != StoreDeviceId::derive(&registration.store_root, &registration.origin)
535        {
536            return Err(StoreProtocolError::Malformed(
537                "Store device id differs from its root and origin".to_string(),
538            ));
539        }
540        validate_acknowledgement_anchor(&registration.acknowledgements)?;
541        let author_pubkey = registration.author_pubkey.clone();
542        registration.verify_by(&author_pubkey)?;
543        Ok(registration)
544    }
545}
546
547fn derive_device_signer(
548    identity_signer: &UserKeypair,
549    store_root: &StoreRootRef,
550    origin: &StoreDeviceRegistrationOrigin,
551) -> UserKeypair {
552    const DOMAIN: &[u8] = b"coven.store-device-signing-key.v1\0";
553    let context = serde_json::to_vec(&(store_root, origin))
554        .expect("Store device signing context serialization cannot fail");
555    identity_signer.derive_signing_key(DOMAIN, &context)
556}
557
558fn validate_acknowledgement_anchor(
559    acknowledgements: &DeviceStreamAnchor,
560) -> Result<(), StoreProtocolError> {
561    if !matches!(
562        acknowledgements,
563        DeviceStreamAnchor::StoreAcknowledgements { .. }
564    ) {
565        return Err(StoreProtocolError::Malformed(
566            "Store device registration contains a mismatched acknowledgement anchor".to_string(),
567        ));
568    }
569    Ok(())
570}