Skip to main content

coven_core/sync/store_commit/
device_join.rs

1use super::validation::require_version;
2use super::*;
3
4const STORE_DEVICE_ID_DOMAIN: &[u8] = b"coven.store-device-id.v1\0";
5
6/// The stable identity of one device in a Store, derived from the Store root and
7/// the device's registration origin. It names a device across the protocol — in
8/// membership, commit authorship, and epoch-close participation — and is what
9/// `Circles::exclude_close_device` and `Circles::close_status` address.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
11#[serde(transparent)]
12pub struct StoreDeviceId(ObjectHash);
13
14impl StoreDeviceId {
15    pub fn derive(store_root: &StoreRootRef, origin: &StoreDeviceRegistrationOrigin) -> Self {
16        let mut material = STORE_DEVICE_ID_DOMAIN.to_vec();
17        material.extend(
18            serde_json::to_vec(&(store_root, origin.external_id()))
19                .expect("Store device identity serialization cannot fail"),
20        );
21        Self(ObjectHash::digest(&material))
22    }
23}
24
25impl fmt::Display for StoreDeviceId {
26    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
27        fmt::Display::fmt(&self.0, formatter)
28    }
29}
30
31impl FromStr for StoreDeviceId {
32    type Err = StoreProtocolError;
33
34    fn from_str(value: &str) -> Result<Self, Self::Err> {
35        value.parse().map(Self)
36    }
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
40#[serde(transparent)]
41pub struct StoreCreationId(ObjectHash);
42
43impl StoreCreationId {
44    pub fn from_random_bytes(bytes: [u8; 32]) -> Self {
45        Self(ObjectHash::from_digest(bytes))
46    }
47
48    #[cfg(any(test, feature = "test-utils"))]
49    pub(crate) fn from_nonce(nonce: &str) -> Self {
50        Self(ObjectHash::digest(nonce.as_bytes()))
51    }
52
53    pub(super) fn object_hash(self) -> ObjectHash {
54        self.0
55    }
56}
57
58impl fmt::Display for StoreCreationId {
59    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
60        fmt::Display::fmt(&self.0, formatter)
61    }
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
65#[serde(transparent)]
66pub struct DeviceJoinAttemptId(ObjectHash);
67
68impl DeviceJoinAttemptId {
69    pub fn from_hash(hash: ObjectHash) -> Self {
70        Self(hash)
71    }
72
73    pub(super) fn object_hash(self) -> ObjectHash {
74        self.0
75    }
76}
77
78impl fmt::Display for DeviceJoinAttemptId {
79    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
80        fmt::Display::fmt(&self.0, formatter)
81    }
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
85#[serde(transparent)]
86pub struct DeviceRecoveryId(ObjectHash);
87
88impl DeviceRecoveryId {
89    pub fn from_hash(hash: ObjectHash) -> Self {
90        Self(hash)
91    }
92
93    pub(super) fn object_hash(self) -> ObjectHash {
94        self.0
95    }
96}
97
98impl fmt::Display for DeviceRecoveryId {
99    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
100        fmt::Display::fmt(&self.0, formatter)
101    }
102}
103
104#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
105#[serde(deny_unknown_fields)]
106pub struct DeviceJoinAttemptRef {
107    pub attempt_id: DeviceJoinAttemptId,
108    pub attempt_hash: ObjectHash,
109    pub object: ExactObjectRef,
110}
111
112#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
113#[serde(deny_unknown_fields)]
114pub struct DeviceJoinAttempt {
115    pub version: u32,
116    pub store_root: StoreRootRef,
117    pub attempt_id: DeviceJoinAttemptId,
118    pub attempt_slot: ObjectSlot,
119    pub expected_registration: StoreDeviceRegistration,
120    pub registration_slot: ObjectSlot,
121    pub outcome_slot: ObjectSlot,
122    pub bootstrap_cut: StoreHistoryCut,
123    pub membership: StoreMembershipStateRef,
124    pub provider_admin_grant: crate::sync::provider::ProviderAdminGrantId,
125    pub provider_approval: crate::sync::store::DeviceProviderAdmissionApproval,
126    pub provider_response: crate::sync::store::DeviceProviderResponseReservation,
127    pub owner_registration: StoreDeviceRegistrationRef,
128    pub owner_grant: MembershipGrantId,
129    pub signature: String,
130}
131
132pub(crate) struct UnverifiedDeviceJoinAttempt(DeviceJoinAttempt);
133
134impl UnverifiedDeviceJoinAttempt {
135    pub(crate) fn verify_at(
136        self,
137        expected: &DeviceJoinAttemptRef,
138        owner: &StoreDeviceRegistration,
139    ) -> Result<DeviceJoinAttempt, StoreProtocolError> {
140        let attempt = self.0;
141        require_version(attempt.version)?;
142        attempt.validate_shape()?;
143        if attempt.attempt_id != expected.attempt_id
144            || attempt.attempt_hash() != expected.attempt_hash
145            || &attempt.attempt_slot != expected.object.slot()
146        {
147            return Err(StoreProtocolError::JoinAttemptMismatch);
148        }
149        attempt.owner_registration.verify_registration(owner)?;
150        if !keys::verify_signature_hex(
151            &owner.device_signing_pubkey,
152            &attempt.signature,
153            &attempt.canonical_signed_bytes(),
154        ) {
155            return Err(StoreProtocolError::InvalidSignature);
156        }
157        Ok(attempt)
158    }
159}
160
161#[derive(Serialize)]
162struct DeviceJoinAttemptSignedFields<'a> {
163    version: u32,
164    store_root: &'a StoreRootRef,
165    attempt_id: DeviceJoinAttemptId,
166    attempt_slot: &'a ObjectSlot,
167    expected_registration: &'a StoreDeviceRegistration,
168    registration_slot: &'a ObjectSlot,
169    outcome_slot: &'a ObjectSlot,
170    bootstrap_cut: &'a StoreHistoryCut,
171    membership: &'a StoreMembershipStateRef,
172    provider_admin_grant: &'a crate::sync::provider::ProviderAdminGrantId,
173    provider_approval: &'a crate::sync::store::DeviceProviderAdmissionApproval,
174    provider_response: &'a crate::sync::store::DeviceProviderResponseReservation,
175    owner_registration: &'a StoreDeviceRegistrationRef,
176    owner_grant: &'a MembershipGrantId,
177}
178
179impl DeviceJoinAttempt {
180    #[allow(clippy::too_many_arguments)]
181    pub fn signed(
182        store_root: StoreRootRef,
183        attempt_id: DeviceJoinAttemptId,
184        attempt_slot: ObjectSlot,
185        expected_registration: StoreDeviceRegistration,
186        registration_slot: ObjectSlot,
187        outcome_slot: ObjectSlot,
188        bootstrap_cut: StoreHistoryCut,
189        membership: StoreMembershipStateRef,
190        provider_admin_grant: crate::sync::provider::ProviderAdminGrantId,
191        provider_approval: crate::sync::store::DeviceProviderAdmissionApproval,
192        provider_response: crate::sync::store::DeviceProviderResponseReservation,
193        owner_registration: StoreDeviceRegistrationRef,
194        owner_grant: MembershipGrantId,
195        owner: &StoreDeviceRegistration,
196        owner_device_signer: &UserKeypair,
197    ) -> Result<Self, StoreProtocolError> {
198        owner_registration.verify_registration(owner)?;
199        if keys::public_key_hex(owner_device_signer) != owner.device_signing_pubkey {
200            return Err(StoreProtocolError::InvalidSignature);
201        }
202        let mut attempt = Self {
203            version: STORE_PROTOCOL_VERSION,
204            store_root,
205            attempt_id,
206            attempt_slot,
207            expected_registration,
208            registration_slot,
209            outcome_slot,
210            bootstrap_cut,
211            membership,
212            provider_admin_grant,
213            provider_approval,
214            provider_response,
215            owner_registration,
216            owner_grant,
217            signature: String::new(),
218        };
219        attempt.validate_shape()?;
220        let (_, signature) = keys::sign_hex(owner_device_signer, &attempt.canonical_signed_bytes());
221        attempt.signature = signature;
222        Ok(attempt)
223    }
224
225    fn canonical_signed_bytes(&self) -> Vec<u8> {
226        domain_json(
227            DEVICE_JOIN_ATTEMPT_DOMAIN,
228            &DeviceJoinAttemptSignedFields {
229                version: self.version,
230                store_root: &self.store_root,
231                attempt_id: self.attempt_id,
232                attempt_slot: &self.attempt_slot,
233                expected_registration: &self.expected_registration,
234                registration_slot: &self.registration_slot,
235                outcome_slot: &self.outcome_slot,
236                bootstrap_cut: &self.bootstrap_cut,
237                membership: &self.membership,
238                provider_admin_grant: &self.provider_admin_grant,
239                provider_approval: &self.provider_approval,
240                provider_response: &self.provider_response,
241                owner_registration: &self.owner_registration,
242                owner_grant: &self.owner_grant,
243            },
244        )
245    }
246
247    pub fn attempt_hash(&self) -> ObjectHash {
248        ObjectHash::digest(&self.canonical_signed_bytes())
249    }
250
251    pub fn to_bytes(&self) -> Vec<u8> {
252        serde_json::to_vec(self).expect("DeviceJoinAttempt serialization cannot fail")
253    }
254
255    pub fn parse_at(
256        bytes: &[u8],
257        expected: &DeviceJoinAttemptRef,
258        owner: &StoreDeviceRegistration,
259    ) -> Result<Self, StoreProtocolError> {
260        Self::parse_unverified(bytes)?.verify_at(expected, owner)
261    }
262
263    pub(crate) fn parse_unverified(
264        bytes: &[u8],
265    ) -> Result<UnverifiedDeviceJoinAttempt, StoreProtocolError> {
266        serde_json::from_slice(bytes)
267            .map(UnverifiedDeviceJoinAttempt)
268            .map_err(|error| StoreProtocolError::Malformed(error.to_string()))
269    }
270
271    fn validate_shape(&self) -> Result<(), StoreProtocolError> {
272        validate_store_history_cut(&self.bootstrap_cut)?;
273        if self.expected_registration.store_root != self.store_root
274            || self.expected_registration.device_id
275                != StoreDeviceId::derive(&self.store_root, &self.expected_registration.origin)
276            || self.attempt_slot == self.registration_slot
277            || self.attempt_slot == self.outcome_slot
278            || self.registration_slot == self.outcome_slot
279            || self.provider_admin_grant
280                != self.provider_approval.request.offer.provider_admin.grant_id
281            || self.provider_approval.request.offer.store_root != self.store_root
282            || self.provider_approval.request.offer.attempt_id != self.attempt_id
283            || self.provider_approval.request.offer.attempt_slot != self.attempt_slot
284            || self.provider_approval.request.offer.outcome_slot != self.outcome_slot
285            || self.provider_approval.request.offer.owner_registration != self.owner_registration
286            || self.provider_approval.request.offer.owner_grant != self.owner_grant
287            || self.provider_approval.request.offer.member_pubkey
288                != self.expected_registration.author_pubkey
289            || self.provider_approval.request.peer_provider != self.expected_registration.provider
290        {
291            return Err(StoreProtocolError::JoinAttemptMismatch);
292        }
293        match (&self.provider_approval.admission, &self.provider_response) {
294            (
295                crate::sync::store::DeviceProviderAdmissionChallenge::SamePrincipal,
296                crate::sync::store::DeviceProviderResponseReservation::SamePrincipal,
297            )
298            | (
299                crate::sync::store::DeviceProviderAdmissionChallenge::CrossPrincipal(_),
300                crate::sync::store::DeviceProviderResponseReservation::CrossPrincipal { .. },
301            ) => {}
302            _ => return Err(StoreProtocolError::JoinAttemptMismatch),
303        }
304        match &self.expected_registration.origin {
305            StoreDeviceRegistrationOrigin::Join {
306                attempt_id,
307                attempt_slot,
308                outcome_slot,
309            } if *attempt_id == self.attempt_id
310                && attempt_slot == &self.attempt_slot
311                && outcome_slot == &self.outcome_slot =>
312            {
313                Ok(())
314            }
315            _ => Err(StoreProtocolError::JoinAttemptMismatch),
316        }
317    }
318}
319
320#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
321#[serde(deny_unknown_fields)]
322pub struct DeviceReadinessProof {
323    pub version: u32,
324    pub store_root_hash: ObjectHash,
325    pub attempt: DeviceJoinAttemptRef,
326    pub registration: StoreDeviceRegistrationRef,
327    pub initial_ack: StoreAckRef,
328    pub bootstrap_cut: StoreHistoryCut,
329    pub signature: String,
330}
331
332#[derive(Serialize)]
333struct DeviceReadinessSignedFields<'a> {
334    version: u32,
335    store_root_hash: ObjectHash,
336    attempt: &'a DeviceJoinAttemptRef,
337    registration: &'a StoreDeviceRegistrationRef,
338    initial_ack: &'a StoreAckRef,
339    bootstrap_cut: &'a StoreHistoryCut,
340}
341
342impl DeviceReadinessProof {
343    pub fn signed(
344        attempt: DeviceJoinAttemptRef,
345        registration: StoreDeviceRegistrationRef,
346        initial_ack: StoreAckRef,
347        bootstrap_cut: StoreHistoryCut,
348        registration_value: &StoreDeviceRegistration,
349        device_signer: &UserKeypair,
350    ) -> Result<Self, StoreProtocolError> {
351        registration.verify_registration(registration_value)?;
352        if keys::public_key_hex(device_signer) != registration_value.device_signing_pubkey {
353            return Err(StoreProtocolError::InvalidSignature);
354        }
355        let mut proof = Self {
356            version: STORE_PROTOCOL_VERSION,
357            store_root_hash: registration_value.store_root.store_root_hash,
358            attempt,
359            registration,
360            initial_ack,
361            bootstrap_cut,
362            signature: String::new(),
363        };
364        validate_store_history_cut(&proof.bootstrap_cut)?;
365        let (_, signature) = keys::sign_hex(device_signer, &proof.canonical_signed_bytes());
366        proof.signature = signature;
367        Ok(proof)
368    }
369
370    fn canonical_signed_bytes(&self) -> Vec<u8> {
371        domain_json(
372            DEVICE_READINESS_DOMAIN,
373            &DeviceReadinessSignedFields {
374                version: self.version,
375                store_root_hash: self.store_root_hash,
376                attempt: &self.attempt,
377                registration: &self.registration,
378                initial_ack: &self.initial_ack,
379                bootstrap_cut: &self.bootstrap_cut,
380            },
381        )
382    }
383
384    pub fn verify(
385        &self,
386        attempt_ref: &DeviceJoinAttemptRef,
387        attempt: &DeviceJoinAttempt,
388        registration: &StoreDeviceRegistration,
389        initial_ack_ref: &StoreAckRef,
390        initial_ack: &StoreAck,
391    ) -> Result<(), StoreProtocolError> {
392        require_version(self.version)?;
393        if &self.attempt != attempt_ref
394            || attempt_ref.attempt_id != attempt.attempt_id
395            || attempt_ref.attempt_hash != attempt.attempt_hash()
396            || self.store_root_hash != registration.store_root.store_root_hash
397            || self.registration.device_id != registration.device_id
398            || self.bootstrap_cut != attempt.bootstrap_cut
399        {
400            return Err(StoreProtocolError::DeviceReadinessMismatch);
401        }
402        self.registration.verify_registration(registration)?;
403        if initial_ack.registration != self.registration
404            || initial_ack.sequence != 1
405            || initial_ack.successor.predecessor.is_some()
406            || initial_ack_ref != &self.initial_ack
407            || initial_ack_ref.registration != self.registration
408            || initial_ack_ref.sequence != initial_ack.sequence
409            || initial_ack_ref.ack_hash != initial_ack.ack_hash()
410            || initial_ack.store_cut != self.bootstrap_cut
411        {
412            return Err(StoreProtocolError::DeviceReadinessMismatch);
413        }
414        validate_store_history_cut(&self.bootstrap_cut)?;
415        if !keys::verify_signature_hex(
416            &registration.device_signing_pubkey,
417            &self.signature,
418            &self.canonical_signed_bytes(),
419        ) {
420            return Err(StoreProtocolError::InvalidSignature);
421        }
422        Ok(())
423    }
424}
425
426#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
427#[serde(rename_all = "snake_case", deny_unknown_fields)]
428pub enum DeviceJoinOutcomeBody {
429    Activated { readiness: DeviceReadinessProof },
430    Cancelled,
431}
432
433#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
434#[serde(deny_unknown_fields)]
435pub struct DeviceJoinOutcome {
436    pub version: u32,
437    pub store_root_hash: ObjectHash,
438    pub attempt: DeviceJoinAttemptRef,
439    pub body: DeviceJoinOutcomeBody,
440    pub owner_registration: StoreDeviceRegistrationRef,
441    pub owner_grant: MembershipGrantId,
442    pub signature: String,
443}
444
445#[derive(Serialize)]
446struct DeviceJoinOutcomeSignedFields<'a> {
447    version: u32,
448    store_root_hash: ObjectHash,
449    attempt: &'a DeviceJoinAttemptRef,
450    body: &'a DeviceJoinOutcomeBody,
451    owner_registration: &'a StoreDeviceRegistrationRef,
452    owner_grant: &'a MembershipGrantId,
453}
454
455impl DeviceJoinOutcome {
456    pub fn signed(
457        attempt: DeviceJoinAttemptRef,
458        body: DeviceJoinOutcomeBody,
459        owner_registration: StoreDeviceRegistrationRef,
460        owner_grant: MembershipGrantId,
461        owner: &StoreDeviceRegistration,
462        owner_device_signer: &UserKeypair,
463    ) -> Result<Self, StoreProtocolError> {
464        owner_registration.verify_registration(owner)?;
465        if keys::public_key_hex(owner_device_signer) != owner.device_signing_pubkey {
466            return Err(StoreProtocolError::InvalidSignature);
467        }
468        let mut outcome = Self {
469            version: STORE_PROTOCOL_VERSION,
470            store_root_hash: owner.store_root.store_root_hash,
471            attempt,
472            body,
473            owner_registration,
474            owner_grant,
475            signature: String::new(),
476        };
477        let (_, signature) = keys::sign_hex(owner_device_signer, &outcome.canonical_signed_bytes());
478        outcome.signature = signature;
479        Ok(outcome)
480    }
481
482    pub(crate) fn canonical_signed_bytes(&self) -> Vec<u8> {
483        domain_json(
484            DEVICE_JOIN_OUTCOME_DOMAIN,
485            &DeviceJoinOutcomeSignedFields {
486                version: self.version,
487                store_root_hash: self.store_root_hash,
488                attempt: &self.attempt,
489                body: &self.body,
490                owner_registration: &self.owner_registration,
491                owner_grant: &self.owner_grant,
492            },
493        )
494    }
495
496    pub fn outcome_hash(&self) -> ObjectHash {
497        ObjectHash::digest(&self.canonical_signed_bytes())
498    }
499
500    pub fn to_bytes(&self) -> Vec<u8> {
501        serde_json::to_vec(self).expect("DeviceJoinOutcome serialization cannot fail")
502    }
503}
504
505#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
506#[serde(rename_all = "snake_case", deny_unknown_fields)]
507pub enum DeviceJoinOutcomeRef {
508    Activated {
509        attempt: DeviceJoinAttemptRef,
510        outcome_hash: ObjectHash,
511        object: ExactObjectRef,
512    },
513    Cancelled {
514        attempt: DeviceJoinAttemptRef,
515        outcome_hash: ObjectHash,
516        object: ExactObjectRef,
517    },
518}
519
520impl DeviceJoinOutcomeRef {
521    pub fn slot(&self) -> &ObjectSlot {
522        self.object().slot()
523    }
524
525    pub fn object(&self) -> &ExactObjectRef {
526        match self {
527            Self::Activated { object, .. } | Self::Cancelled { object, .. } => object,
528        }
529    }
530
531    pub fn attempt(&self) -> &DeviceJoinAttemptRef {
532        match self {
533            Self::Activated { attempt, .. } | Self::Cancelled { attempt, .. } => attempt,
534        }
535    }
536
537    pub fn verify_outcome(&self, outcome: &DeviceJoinOutcome) -> Result<(), StoreProtocolError> {
538        let (attempt, expected_hash, expects_activated) = match self {
539            Self::Activated {
540                attempt,
541                outcome_hash,
542                ..
543            } => (attempt, outcome_hash, true),
544            Self::Cancelled {
545                attempt,
546                outcome_hash,
547                ..
548            } => (attempt, outcome_hash, false),
549        };
550        if &outcome.attempt != attempt || outcome.outcome_hash() != *expected_hash {
551            return Err(StoreProtocolError::JoinOutcomeMismatch);
552        }
553        if expects_activated != matches!(outcome.body, DeviceJoinOutcomeBody::Activated { .. }) {
554            return Err(StoreProtocolError::JoinOutcomeMismatch);
555        }
556        Ok(())
557    }
558}
559
560#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
561#[serde(deny_unknown_fields)]
562pub struct OwnerRecoveryNodeRef {
563    pub owner_pubkey: String,
564    pub owner_grant: MembershipGrantId,
565    pub sequence: u64,
566    pub node_hash: ObjectHash,
567    pub object: ExactObjectRef,
568}
569
570#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
571#[serde(transparent)]
572pub struct OwnerRecoveryActivationId(ObjectHash);
573
574impl OwnerRecoveryActivationId {
575    pub fn derive(
576        root: &StoreRootRef,
577        owner_pubkey: &str,
578        owner_grant: &MembershipGrantId,
579        anchor: &GrantStreamAnchor,
580    ) -> Result<Self, StoreProtocolError> {
581        if !matches!(anchor, GrantStreamAnchor::OwnerRecovery { .. }) {
582            return Err(StoreProtocolError::OwnerRecoveryMismatch);
583        }
584        Ok(Self(ObjectHash::digest(&domain_json(
585            b"coven.owner-recovery-activation.v1\0",
586            &(root, owner_pubkey, owner_grant, anchor),
587        ))))
588    }
589}
590
591#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
592#[serde(rename_all = "snake_case", deny_unknown_fields)]
593pub enum OwnerRecoveryPosition {
594    BeforeFirst {
595        activation: OwnerRecoveryActivationId,
596    },
597    At {
598        node: OwnerRecoveryNodeRef,
599    },
600}
601
602#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
603#[serde(deny_unknown_fields)]
604pub struct OwnerRecoveryCursor {
605    pub owner_grant: MembershipGrantId,
606    pub position: OwnerRecoveryPosition,
607}