Skip to main content

coven_core/sync/store/device_join/
exchange.rs

1use super::cleanup::{require_cancelled_outcome, validate_terminals};
2use super::journal::require_distinct_slots;
3use super::*;
4
5const OFFER_DOMAIN: &[u8] = b"coven.device-join-offer.v1\0";
6const ACCESS_REQUEST_DOMAIN: &[u8] = b"coven.device-provider-access-request.v1\0";
7const APPROVAL_DOMAIN: &[u8] = b"coven.device-provider-admission-approval.v1\0";
8const REGISTRATION_REQUEST_DOMAIN: &[u8] = b"coven.device-registration-request.v1\0";
9const ABANDONMENT_DOMAIN: &[u8] = b"coven.device-join-abandonment.v1\0";
10const PROVIDER_CLOSURE_DOMAIN: &[u8] = b"coven.device-join-provider-closure.v1\0";
11const JOINER_CLOSURE_DOMAIN: &[u8] = b"coven.device-join-joiner-closure.v1\0";
12const WRITE_REVOCATION_DOMAIN: &[u8] = b"coven.device-join-write-revocation.v1\0";
13const CLEANUP_RECEIPT_DOMAIN: &[u8] = b"coven.device-join-cleanup-receipt.v1\0";
14
15#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
16#[serde(deny_unknown_fields)]
17pub struct DeviceJoinOffer {
18    pub version: u32,
19    pub attempt_id: DeviceJoinAttemptId,
20    pub member_pubkey: String,
21    pub store_root: StoreRootRef,
22    pub provider: StoreProviderBinding,
23    pub attempt_slot: ObjectSlot,
24    pub outcome_slot: ObjectSlot,
25    pub owner_registration: StoreDeviceRegistrationRef,
26    pub owner_grant: MembershipGrantId,
27    pub provider_admin: Box<ProviderAdminGrantRecord>,
28    pub signature: String,
29}
30
31impl DeviceJoinOffer {
32    #[allow(clippy::too_many_arguments)]
33    pub fn signed(
34        attempt_id: DeviceJoinAttemptId,
35        member_pubkey: String,
36        store_root: StoreRootRef,
37        provider: StoreProviderBinding,
38        attempt_slot: ObjectSlot,
39        outcome_slot: ObjectSlot,
40        owner_registration: StoreDeviceRegistrationRef,
41        owner_grant: MembershipGrantId,
42        provider_admin: ProviderAdminGrantRecord,
43        owner: &StoreDeviceRegistration,
44        owner_device_signer: &UserKeypair,
45    ) -> Result<Self, DeviceJoinError> {
46        owner_registration.verify_registration(owner)?;
47        if keys::public_key_hex(owner_device_signer) != owner.device_signing_pubkey {
48            return Err(DeviceJoinError::InvalidSignature);
49        }
50        let mut value = Self {
51            version: STORE_PROTOCOL_VERSION,
52            attempt_id,
53            member_pubkey,
54            store_root,
55            provider,
56            attempt_slot,
57            outcome_slot,
58            owner_registration,
59            owner_grant,
60            provider_admin: Box::new(provider_admin),
61            signature: String::new(),
62        };
63        value.validate_shape()?;
64        value.signature = sign(owner_device_signer, OFFER_DOMAIN, &value.signed_fields());
65        Ok(value)
66    }
67
68    pub fn verify(&self, owner: &StoreDeviceRegistration) -> Result<(), DeviceJoinError> {
69        self.validate_shape()?;
70        self.owner_registration.verify_registration(owner)?;
71        verify_signature(
72            &owner.device_signing_pubkey,
73            &self.signature,
74            OFFER_DOMAIN,
75            &self.signed_fields(),
76        )
77    }
78
79    pub fn offer_hash(&self) -> ObjectHash {
80        ObjectHash::digest(&domain_json(OFFER_DOMAIN, &self.signed_fields()))
81    }
82
83    fn validate_shape(&self) -> Result<(), DeviceJoinError> {
84        if self.version != STORE_PROTOCOL_VERSION
85            || self.member_pubkey.is_empty()
86            || self.provider_admin.administrator != self.owner_registration
87                && self.provider_admin.administrator.device_id == self.owner_registration.device_id
88            || self.attempt_slot == self.outcome_slot
89        {
90            return Err(DeviceJoinError::OfferMismatch);
91        }
92        self.provider.validate()?;
93        self.provider_admin
94            .provider
95            .validate_for(&self.provider)
96            .map_err(DeviceJoinError::Storage)?;
97        if let crate::sync::provider::ProviderAdminGrantOrigin::Founder { root } =
98            &self.provider_admin.created_at
99        {
100            if root != &self.store_root {
101                return Err(DeviceJoinError::OfferMismatch);
102            }
103        }
104        Ok(())
105    }
106
107    fn signed_fields(&self) -> DeviceJoinOfferFields<'_> {
108        DeviceJoinOfferFields {
109            version: self.version,
110            attempt_id: self.attempt_id,
111            member_pubkey: &self.member_pubkey,
112            store_root: &self.store_root,
113            provider: &self.provider,
114            attempt_slot: &self.attempt_slot,
115            outcome_slot: &self.outcome_slot,
116            owner_registration: &self.owner_registration,
117            owner_grant: &self.owner_grant,
118            provider_admin: &self.provider_admin,
119        }
120    }
121}
122
123#[derive(Serialize)]
124struct DeviceJoinOfferFields<'a> {
125    version: u32,
126    attempt_id: DeviceJoinAttemptId,
127    member_pubkey: &'a str,
128    store_root: &'a StoreRootRef,
129    provider: &'a StoreProviderBinding,
130    attempt_slot: &'a ObjectSlot,
131    outcome_slot: &'a ObjectSlot,
132    owner_registration: &'a StoreDeviceRegistrationRef,
133    owner_grant: &'a MembershipGrantId,
134    provider_admin: &'a ProviderAdminGrantRecord,
135}
136
137#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
138#[serde(deny_unknown_fields)]
139pub struct DeviceProviderAccessRequest {
140    pub offer: Box<DeviceJoinOffer>,
141    pub peer_provider: ProviderDeviceBinding,
142    pub signature: String,
143}
144
145impl DeviceProviderAccessRequest {
146    pub fn signed(
147        offer: DeviceJoinOffer,
148        peer_provider: ProviderDeviceBinding,
149        member_signer: &UserKeypair,
150    ) -> Result<Self, DeviceJoinError> {
151        if keys::public_key_hex(member_signer) != offer.member_pubkey {
152            return Err(DeviceJoinError::InvalidSignature);
153        }
154        peer_provider.validate_for(&offer.provider)?;
155        let mut request = Self {
156            offer: Box::new(offer),
157            peer_provider,
158            signature: String::new(),
159        };
160        request.signature = sign(
161            member_signer,
162            ACCESS_REQUEST_DOMAIN,
163            &request.signed_fields(),
164        );
165        Ok(request)
166    }
167
168    pub fn verify(&self, owner: &StoreDeviceRegistration) -> Result<(), DeviceJoinError> {
169        self.offer.verify(owner)?;
170        self.peer_provider.validate_for(&self.offer.provider)?;
171        verify_signature(
172            &self.offer.member_pubkey,
173            &self.signature,
174            ACCESS_REQUEST_DOMAIN,
175            &self.signed_fields(),
176        )
177    }
178
179    pub fn request_hash(&self) -> ObjectHash {
180        ObjectHash::digest(&domain_json(ACCESS_REQUEST_DOMAIN, &self.signed_fields()))
181    }
182
183    fn signed_fields(&self) -> (&DeviceJoinOffer, &ProviderDeviceBinding) {
184        (&self.offer, &self.peer_provider)
185    }
186}
187
188#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
189#[serde(rename_all = "snake_case", deny_unknown_fields)]
190pub enum DeviceProviderAdmissionChallenge {
191    SamePrincipal,
192    CrossPrincipal(CrossPrincipalProbeChallenge),
193}
194
195#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
196#[serde(deny_unknown_fields)]
197pub struct DeviceProviderAdmissionApproval {
198    pub request: Box<DeviceProviderAccessRequest>,
199    pub access_grant: ActivatedStoreMemberProviderAccessGrant,
200    pub admission: DeviceProviderAdmissionChallenge,
201    pub signature: String,
202}
203
204impl DeviceProviderAdmissionApproval {
205    pub(crate) fn signed(
206        request: DeviceProviderAccessRequest,
207        access_grant: ActivatedStoreMemberProviderAccessGrant,
208        admission: DeviceProviderAdmissionChallenge,
209        store_root: &crate::sync::store_objects::VerifiedObject<StoreProtocolRoot>,
210        administrator: &StoreDeviceRegistration,
211        administrator_device_signer: &UserKeypair,
212    ) -> Result<Self, DeviceJoinError> {
213        if keys::public_key_hex(administrator_device_signer) != administrator.device_signing_pubkey
214        {
215            return Err(DeviceJoinError::InvalidSignature);
216        }
217        let mut value = Self {
218            request: Box::new(request),
219            access_grant,
220            admission,
221            signature: String::new(),
222        };
223        value.validate_shape(store_root, administrator)?;
224        value.sign_with(administrator_device_signer);
225        Ok(value)
226    }
227
228    #[cfg(test)]
229    pub(crate) fn signed_without_shape_validation_for_test(
230        request: DeviceProviderAccessRequest,
231        access_grant: ActivatedStoreMemberProviderAccessGrant,
232        admission: DeviceProviderAdmissionChallenge,
233        administrator_device_signer: &UserKeypair,
234    ) -> Self {
235        let mut value = Self {
236            request: Box::new(request),
237            access_grant,
238            admission,
239            signature: String::new(),
240        };
241        value.sign_with(administrator_device_signer);
242        value
243    }
244
245    fn sign_with(&mut self, administrator_device_signer: &UserKeypair) {
246        self.signature = sign(
247            administrator_device_signer,
248            APPROVAL_DOMAIN,
249            &self.signed_fields(),
250        );
251    }
252
253    pub(in crate::sync::store) fn verify(
254        &self,
255        store_root: &crate::sync::store_objects::VerifiedObject<StoreProtocolRoot>,
256        owner: &StoreDeviceRegistration,
257        administrator: &StoreDeviceRegistration,
258    ) -> Result<(), DeviceJoinError> {
259        self.request.verify(owner)?;
260        self.validate_shape(store_root, administrator)?;
261        verify_signature(
262            &administrator.device_signing_pubkey,
263            &self.signature,
264            APPROVAL_DOMAIN,
265            &self.signed_fields(),
266        )
267    }
268
269    fn validate_shape(
270        &self,
271        store_root: &crate::sync::store_objects::VerifiedObject<StoreProtocolRoot>,
272        administrator: &StoreDeviceRegistration,
273    ) -> Result<(), DeviceJoinError> {
274        let offer = &self.request.offer;
275        if store_root.object != offer.store_root.object
276            || store_root.value.object_hash() != offer.store_root.store_root_hash
277            || store_root.value.descriptor.store_root_id() != offer.store_root.store_root_id
278            || store_root.value.descriptor.provider != offer.provider
279            || self.access_grant.grant.member_pubkey != offer.member_pubkey
280            || self.access_grant.grant.provider != self.request.peer_provider
281            || self.access_grant.grant_ref.grant_id != self.access_grant.grant.grant_id
282            || self.access_grant.grant_ref.grant_hash != self.access_grant.grant.grant_hash()
283            || self.access_grant.grant.administrator_grant != offer.provider_admin.grant_id
284            || self.access_grant.grant.administrator != offer.provider_admin.administrator
285        {
286            return Err(DeviceJoinError::ApprovalMismatch);
287        }
288        self.access_grant
289            .grant
290            .verify(&offer.provider, administrator)
291            .map_err(|error| DeviceJoinError::Provider(error.to_string()))?;
292        let same_principal = offer.provider_admin.provider == self.request.peer_provider;
293        if same_principal
294            != matches!(
295                self.admission,
296                DeviceProviderAdmissionChallenge::SamePrincipal
297            )
298        {
299            return Err(DeviceJoinError::ApprovalMismatch);
300        }
301        Ok(())
302    }
303
304    fn signed_fields(
305        &self,
306    ) -> (
307        &DeviceProviderAccessRequest,
308        &ActivatedStoreMemberProviderAccessGrant,
309        &DeviceProviderAdmissionChallenge,
310    ) {
311        (&self.request, &self.access_grant, &self.admission)
312    }
313}
314
315#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
316#[serde(rename_all = "snake_case", deny_unknown_fields)]
317pub enum DeviceProviderResponseReservation {
318    SamePrincipal,
319    CrossPrincipal { response_slot: ObjectSlot },
320}
321
322#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
323#[serde(deny_unknown_fields)]
324pub struct DeviceRegistrationRequest {
325    pub approval: Box<DeviceProviderAdmissionApproval>,
326    pub expected_registration: StoreDeviceRegistration,
327    pub registration_slot: ObjectSlot,
328    pub response: DeviceProviderResponseReservation,
329    pub signature: String,
330}
331
332impl DeviceRegistrationRequest {
333    pub fn signed(
334        approval: DeviceProviderAdmissionApproval,
335        expected_registration: StoreDeviceRegistration,
336        registration_slot: ObjectSlot,
337        response: DeviceProviderResponseReservation,
338        member_signer: &UserKeypair,
339    ) -> Result<Self, DeviceJoinError> {
340        if keys::public_key_hex(member_signer) != approval.request.offer.member_pubkey {
341            return Err(DeviceJoinError::InvalidSignature);
342        }
343        let mut value = Self {
344            approval: Box::new(approval),
345            expected_registration,
346            registration_slot,
347            response,
348            signature: String::new(),
349        };
350        value.validate_shape()?;
351        value.signature = sign(
352            member_signer,
353            REGISTRATION_REQUEST_DOMAIN,
354            &value.signed_fields(),
355        );
356        Ok(value)
357    }
358
359    pub fn verify(&self) -> Result<(), DeviceJoinError> {
360        self.validate_shape()?;
361        verify_signature(
362            &self.approval.request.offer.member_pubkey,
363            &self.signature,
364            REGISTRATION_REQUEST_DOMAIN,
365            &self.signed_fields(),
366        )
367    }
368
369    fn validate_shape(&self) -> Result<(), DeviceJoinError> {
370        let offer = &self.approval.request.offer;
371        if self.expected_registration.store_root != offer.store_root
372            || self.expected_registration.author_pubkey != offer.member_pubkey
373            || self.expected_registration.provider != self.approval.request.peer_provider
374            || self.registration_slot == offer.attempt_slot
375            || self.registration_slot == offer.outcome_slot
376        {
377            return Err(DeviceJoinError::RegistrationRequestMismatch);
378        }
379        match (
380            &self.approval.admission,
381            &self.response,
382            &self.expected_registration.origin,
383        ) {
384            (
385                DeviceProviderAdmissionChallenge::SamePrincipal,
386                DeviceProviderResponseReservation::SamePrincipal,
387                crate::sync::store_commit::StoreDeviceRegistrationOrigin::Join {
388                    attempt_id,
389                    attempt_slot,
390                    outcome_slot,
391                },
392            )
393            | (
394                DeviceProviderAdmissionChallenge::CrossPrincipal(_),
395                DeviceProviderResponseReservation::CrossPrincipal { .. },
396                crate::sync::store_commit::StoreDeviceRegistrationOrigin::Join {
397                    attempt_id,
398                    attempt_slot,
399                    outcome_slot,
400                },
401            ) if *attempt_id == offer.attempt_id
402                && attempt_slot == &offer.attempt_slot
403                && outcome_slot == &offer.outcome_slot => {}
404            _ => return Err(DeviceJoinError::RegistrationRequestMismatch),
405        }
406        let mut slots = vec![
407            offer.attempt_slot.clone(),
408            offer.outcome_slot.clone(),
409            self.registration_slot.clone(),
410            self.expected_registration
411                .acknowledgements
412                .first_slot()
413                .clone(),
414        ];
415        if let DeviceProviderAdmissionChallenge::CrossPrincipal(challenge) =
416            &self.approval.admission
417        {
418            slots.push(challenge.administrator_object.slot.clone());
419        }
420        if let DeviceProviderResponseReservation::CrossPrincipal { response_slot } = &self.response
421        {
422            slots.push(response_slot.clone());
423        }
424        require_distinct_slots(&slots)?;
425        Ok(())
426    }
427
428    fn signed_fields(
429        &self,
430    ) -> (
431        &DeviceProviderAdmissionApproval,
432        &StoreDeviceRegistration,
433        &ObjectSlot,
434        &DeviceProviderResponseReservation,
435    ) {
436        (
437            &self.approval,
438            &self.expected_registration,
439            &self.registration_slot,
440            &self.response,
441        )
442    }
443}
444
445#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
446#[serde(deny_unknown_fields)]
447pub struct ProvisionalDeviceBootstrap {
448    pub request: Box<DeviceRegistrationRequest>,
449    pub publication_authorization: DeviceJoinChallengePublicationAuthorization,
450}
451
452#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
453#[serde(rename_all = "snake_case", deny_unknown_fields)]
454pub enum DeviceProviderChallengePublication {
455    SamePrincipal,
456    CrossPrincipal {
457        challenge: CrossPrincipalProbeChallenge,
458    },
459}
460
461#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
462#[serde(deny_unknown_fields)]
463pub struct ProviderReadyDeviceBootstrap {
464    pub bootstrap: Box<ProvisionalDeviceBootstrap>,
465    pub challenge_publication: DeviceProviderChallengePublication,
466}
467
468#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
469#[serde(rename_all = "snake_case", deny_unknown_fields)]
470pub enum DeviceProviderReadiness {
471    SamePrincipal,
472    CrossPrincipal(CrossPrincipalProbeResponse),
473}
474
475#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
476#[serde(deny_unknown_fields)]
477pub struct DeviceJoinReadiness {
478    pub proof: DeviceReadinessProof,
479    pub provider: DeviceProviderReadiness,
480}
481
482#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
483#[serde(rename_all = "snake_case", deny_unknown_fields)]
484pub enum DeviceProviderAdmission {
485    SamePrincipal,
486    CrossPrincipal(CrossPrincipalProbeReceipt),
487}
488
489#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
490#[serde(deny_unknown_fields)]
491pub struct DeviceProviderAdmissionCompletion {
492    pub readiness: Box<DeviceJoinReadiness>,
493    pub admission: DeviceProviderAdmission,
494}
495
496#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
497#[serde(deny_unknown_fields)]
498pub struct DeviceJoinActivation {
499    pub outcome: DeviceJoinOutcomeRef,
500    pub outcome_activation: StoreBatchCommitRef,
501}
502
503#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
504#[serde(deny_unknown_fields)]
505pub struct DeviceJoinCancellation {
506    pub outcome: DeviceJoinOutcomeRef,
507    pub outcome_activation: StoreBatchCommitRef,
508}
509
510#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
511#[serde(deny_unknown_fields)]
512pub struct DeviceJoinAbandonmentRef {
513    pub attempt_id: DeviceJoinAttemptId,
514    pub abandonment_hash: ObjectHash,
515    pub object: ExactObjectRef,
516}
517
518impl DeviceJoinAbandonmentRef {
519    pub(in crate::sync::store) fn verify(
520        &self,
521        abandonment: &DeviceJoinAbandonmentObject,
522        owner: &StoreDeviceRegistration,
523    ) -> Result<(), DeviceJoinError> {
524        abandonment.owner_registration.verify_registration(owner)?;
525        if self.attempt_id != abandonment.attempt_id
526            || self.abandonment_hash != abandonment.abandonment_hash()
527        {
528            return Err(DeviceJoinError::AttemptMismatch);
529        }
530        verify_signature(
531            &owner.device_signing_pubkey,
532            &abandonment.signature,
533            ABANDONMENT_DOMAIN,
534            &abandonment.signed_fields(),
535        )
536    }
537}
538
539#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
540#[serde(deny_unknown_fields)]
541pub(in crate::sync::store) struct DeviceJoinAbandonmentObject {
542    pub version: u32,
543    pub store_root_hash: ObjectHash,
544    pub offer_hash: ObjectHash,
545    pub attempt_id: DeviceJoinAttemptId,
546    pub attempt_slot: ObjectSlot,
547    pub owner_registration: StoreDeviceRegistrationRef,
548    pub owner_grant: MembershipGrantId,
549    pub signature: String,
550}
551
552impl DeviceJoinAbandonmentObject {
553    pub(in crate::sync::store) fn signed(
554        offer: &DeviceJoinOffer,
555        owner: &StoreDeviceRegistration,
556        owner_device_signer: &UserKeypair,
557    ) -> Result<Self, DeviceJoinError> {
558        offer.verify(owner)?;
559        if keys::public_key_hex(owner_device_signer) != owner.device_signing_pubkey {
560            return Err(DeviceJoinError::InvalidSignature);
561        }
562        let mut value = Self {
563            version: STORE_PROTOCOL_VERSION,
564            store_root_hash: offer.store_root.store_root_hash,
565            offer_hash: offer.offer_hash(),
566            attempt_id: offer.attempt_id,
567            attempt_slot: offer.attempt_slot.clone(),
568            owner_registration: offer.owner_registration.clone(),
569            owner_grant: offer.owner_grant.clone(),
570            signature: String::new(),
571        };
572        value.signature = sign(
573            owner_device_signer,
574            ABANDONMENT_DOMAIN,
575            &value.signed_fields(),
576        );
577        Ok(value)
578    }
579
580    pub(in crate::sync::store) fn abandonment_hash(&self) -> ObjectHash {
581        ObjectHash::digest(&domain_json(ABANDONMENT_DOMAIN, &self.signed_fields()))
582    }
583
584    pub(in crate::sync::store) fn to_bytes(&self) -> Vec<u8> {
585        serde_json::to_vec(self).expect("device join abandonment serialization cannot fail")
586    }
587
588    fn signed_fields(
589        &self,
590    ) -> (
591        u32,
592        ObjectHash,
593        ObjectHash,
594        DeviceJoinAttemptId,
595        &ObjectSlot,
596        &StoreDeviceRegistrationRef,
597        &MembershipGrantId,
598    ) {
599        (
600            self.version,
601            self.store_root_hash,
602            self.offer_hash,
603            self.attempt_id,
604            &self.attempt_slot,
605            &self.owner_registration,
606            &self.owner_grant,
607        )
608    }
609}
610
611#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
612#[serde(deny_unknown_fields)]
613pub struct DeviceJoinAbandonment {
614    pub abandonment: DeviceJoinAbandonmentRef,
615    pub abandonment_activation: StoreBatchCommitRef,
616}
617
618#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
619#[serde(rename_all = "snake_case", deny_unknown_fields)]
620pub enum ProviderChallengeDisposition {
621    SamePrincipal,
622    NeverCreated,
623    Created(ExactObjectRef),
624    AlreadyDeleted(ExactObjectRef),
625}
626
627#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
628#[serde(deny_unknown_fields)]
629pub struct ProviderAdminJoinClosure {
630    pub cancellation: DeviceJoinOutcomeRef,
631    pub administrator_registration: StoreDeviceRegistrationRef,
632    pub challenge: ProviderChallengeDisposition,
633    pub prior_state_hash: ObjectHash,
634    pub signature: String,
635}
636
637impl ProviderAdminJoinClosure {
638    pub fn signed(
639        cancellation: DeviceJoinOutcomeRef,
640        administrator_registration: StoreDeviceRegistrationRef,
641        challenge: ProviderChallengeDisposition,
642        prior_state_hash: ObjectHash,
643        administrator: &StoreDeviceRegistration,
644        signer: &UserKeypair,
645    ) -> Result<Self, DeviceJoinError> {
646        require_cancelled_outcome(&cancellation)?;
647        administrator_registration.verify_registration(administrator)?;
648        if keys::public_key_hex(signer) != administrator.device_signing_pubkey {
649            return Err(DeviceJoinError::InvalidSignature);
650        }
651        let mut value = Self {
652            cancellation,
653            administrator_registration,
654            challenge,
655            prior_state_hash,
656            signature: String::new(),
657        };
658        value.signature = sign(signer, PROVIDER_CLOSURE_DOMAIN, &value.signed_fields());
659        Ok(value)
660    }
661
662    pub fn verify(&self, administrator: &StoreDeviceRegistration) -> Result<(), DeviceJoinError> {
663        require_cancelled_outcome(&self.cancellation)?;
664        self.administrator_registration
665            .verify_registration(administrator)?;
666        verify_signature(
667            &administrator.device_signing_pubkey,
668            &self.signature,
669            PROVIDER_CLOSURE_DOMAIN,
670            &self.signed_fields(),
671        )
672    }
673
674    fn signed_fields(
675        &self,
676    ) -> (
677        &DeviceJoinOutcomeRef,
678        &StoreDeviceRegistrationRef,
679        &ProviderChallengeDisposition,
680        ObjectHash,
681    ) {
682        (
683            &self.cancellation,
684            &self.administrator_registration,
685            &self.challenge,
686            self.prior_state_hash,
687        )
688    }
689}
690
691#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
692#[serde(rename_all = "snake_case", deny_unknown_fields)]
693pub enum SlotDisposition {
694    NeverCreated,
695    Created(ExactObjectRef),
696}
697
698#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
699#[serde(rename_all = "snake_case", deny_unknown_fields)]
700pub enum JoinerResponseDisposition {
701    SamePrincipal,
702    Slot(SlotDisposition),
703}
704
705#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
706#[serde(deny_unknown_fields)]
707pub struct JoinerJoinClosure {
708    pub cancellation: DeviceJoinOutcomeRef,
709    pub expected_registration: StoreDeviceRegistration,
710    pub registration: SlotDisposition,
711    pub initial_ack: SlotDisposition,
712    pub response: JoinerResponseDisposition,
713    pub prior_state_hash: ObjectHash,
714    pub signature: String,
715}
716
717impl JoinerJoinClosure {
718    #[allow(clippy::too_many_arguments)]
719    pub fn signed(
720        cancellation: DeviceJoinOutcomeRef,
721        expected_registration: StoreDeviceRegistration,
722        registration: SlotDisposition,
723        initial_ack: SlotDisposition,
724        response: JoinerResponseDisposition,
725        prior_state_hash: ObjectHash,
726        signer: &UserKeypair,
727    ) -> Result<Self, DeviceJoinError> {
728        require_cancelled_outcome(&cancellation)?;
729        if keys::public_key_hex(signer) != expected_registration.device_signing_pubkey {
730            return Err(DeviceJoinError::InvalidSignature);
731        }
732        let mut value = Self {
733            cancellation,
734            expected_registration,
735            registration,
736            initial_ack,
737            response,
738            prior_state_hash,
739            signature: String::new(),
740        };
741        value.signature = sign(signer, JOINER_CLOSURE_DOMAIN, &value.signed_fields());
742        Ok(value)
743    }
744
745    pub fn verify(&self) -> Result<(), DeviceJoinError> {
746        require_cancelled_outcome(&self.cancellation)?;
747        verify_signature(
748            &self.expected_registration.device_signing_pubkey,
749            &self.signature,
750            JOINER_CLOSURE_DOMAIN,
751            &self.signed_fields(),
752        )
753    }
754
755    fn signed_fields(
756        &self,
757    ) -> (
758        &DeviceJoinOutcomeRef,
759        &StoreDeviceRegistration,
760        &SlotDisposition,
761        &SlotDisposition,
762        &JoinerResponseDisposition,
763        ObjectHash,
764    ) {
765        (
766            &self.cancellation,
767            &self.expected_registration,
768            &self.registration,
769            &self.initial_ack,
770            &self.response,
771            self.prior_state_hash,
772        )
773    }
774}
775
776#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
777#[serde(rename_all = "snake_case")]
778pub enum DeviceJoinProducer {
779    ProviderAdministrator,
780    Joiner,
781}
782
783#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
784#[serde(rename_all = "snake_case", deny_unknown_fields)]
785pub enum ProviderWriteAuthorityRef {
786    ProviderAdministrator(ProviderAdminGrantId),
787    MemberAccess(StoreMemberProviderAccessGrantRef),
788}
789
790#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
791#[serde(deny_unknown_fields)]
792pub struct DeviceJoinProducerWriteRevocation {
793    pub cancellation: DeviceJoinOutcomeRef,
794    pub producer: DeviceJoinProducer,
795    pub authority: ProviderWriteAuthorityRef,
796    pub protected_slots: Vec<ObjectSlot>,
797    pub withdrawal: ProviderAccessWithdrawal,
798    pub executor_grant: ProviderAdminGrantId,
799    pub executor: StoreDeviceRegistrationRef,
800    pub signature: String,
801}
802
803impl DeviceJoinProducerWriteRevocation {
804    pub fn signed(
805        cancellation: DeviceJoinOutcomeRef,
806        producer: DeviceJoinProducer,
807        authority: ProviderWriteAuthorityRef,
808        mut protected_slots: Vec<ObjectSlot>,
809        withdrawal: ProviderAccessWithdrawal,
810        executor_grant: ProviderAdminGrantId,
811        executor: StoreDeviceRegistrationRef,
812        executor_registration: &StoreDeviceRegistration,
813        executor_signer: &UserKeypair,
814    ) -> Result<Self, DeviceJoinError> {
815        require_cancelled_outcome(&cancellation)?;
816        executor.verify_registration(executor_registration)?;
817        if keys::public_key_hex(executor_signer) != executor_registration.device_signing_pubkey {
818            return Err(DeviceJoinError::InvalidSignature);
819        }
820        protected_slots.sort();
821        if protected_slots.is_empty() || protected_slots.windows(2).any(|pair| pair[0] == pair[1]) {
822            return Err(DeviceJoinError::CleanupMismatch);
823        }
824        let mut value = Self {
825            cancellation,
826            producer,
827            authority,
828            protected_slots,
829            withdrawal,
830            executor_grant,
831            executor,
832            signature: String::new(),
833        };
834        value.signature = sign(
835            executor_signer,
836            WRITE_REVOCATION_DOMAIN,
837            &value.signed_fields(),
838        );
839        Ok(value)
840    }
841
842    pub fn verify(&self, executor: &StoreDeviceRegistration) -> Result<(), DeviceJoinError> {
843        require_cancelled_outcome(&self.cancellation)?;
844        self.executor.verify_registration(executor)?;
845        verify_signature(
846            &executor.device_signing_pubkey,
847            &self.signature,
848            WRITE_REVOCATION_DOMAIN,
849            &self.signed_fields(),
850        )
851    }
852
853    fn signed_fields(
854        &self,
855    ) -> (
856        &DeviceJoinOutcomeRef,
857        DeviceJoinProducer,
858        &ProviderWriteAuthorityRef,
859        &[ObjectSlot],
860        &ProviderAccessWithdrawal,
861        &ProviderAdminGrantId,
862        &StoreDeviceRegistrationRef,
863    ) {
864        (
865            &self.cancellation,
866            self.producer,
867            &self.authority,
868            &self.protected_slots,
869            &self.withdrawal,
870            &self.executor_grant,
871            &self.executor,
872        )
873    }
874}
875
876#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
877#[serde(rename_all = "snake_case", deny_unknown_fields)]
878pub enum ProviderAdminJoinTerminal {
879    Completed(DeviceProviderAdmissionCompletion),
880    Cancelled(ProviderAdminJoinClosure),
881    WriteRevoked(DeviceJoinProducerWriteRevocation),
882}
883
884#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
885#[serde(rename_all = "snake_case", deny_unknown_fields)]
886pub enum JoinerJoinTerminal {
887    Ready(DeviceJoinReadiness),
888    Cancelled(JoinerJoinClosure),
889    WriteRevoked(DeviceJoinProducerWriteRevocation),
890}
891
892#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
893#[serde(deny_unknown_fields)]
894pub struct DeviceJoinCleanupReceiptRef {
895    pub attempt_id: DeviceJoinAttemptId,
896    pub receipt_hash: ObjectHash,
897    pub object: ExactObjectRef,
898}
899
900impl DeviceJoinCleanupReceiptRef {
901    pub(in crate::sync::store) fn verify(
902        &self,
903        receipt: &DeviceJoinCleanupReceiptObject,
904        executor: &StoreDeviceRegistration,
905    ) -> Result<(), DeviceJoinError> {
906        receipt.executor.verify_registration(executor)?;
907        if self.attempt_id != receipt.cancellation.attempt().attempt_id
908            || self.receipt_hash != receipt.receipt_hash()
909        {
910            return Err(DeviceJoinError::CleanupMismatch);
911        }
912        verify_signature(
913            &executor.device_signing_pubkey,
914            &receipt.signature,
915            CLEANUP_RECEIPT_DOMAIN,
916            &receipt.signed_fields(),
917        )
918    }
919}
920
921#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
922#[serde(deny_unknown_fields)]
923pub(in crate::sync::store) struct DeviceJoinCleanupReceiptObject {
924    pub version: u32,
925    pub store_root_hash: ObjectHash,
926    pub cancellation: DeviceJoinOutcomeRef,
927    pub administrator_terminal: ProviderAdminJoinTerminal,
928    pub joiner_terminal: JoinerJoinTerminal,
929    pub deleted_slots: Vec<ObjectSlot>,
930    pub membership: StoreMembershipStateRef,
931    pub provider_admin_grant: ProviderAdminGrantId,
932    pub executor: StoreDeviceRegistrationRef,
933    pub signature: String,
934}
935
936impl DeviceJoinCleanupReceiptObject {
937    #[allow(clippy::too_many_arguments)]
938    pub(in crate::sync::store) fn signed(
939        attempt: &DeviceJoinAttempt,
940        cancellation: DeviceJoinOutcomeRef,
941        administrator_terminal: ProviderAdminJoinTerminal,
942        joiner_terminal: JoinerJoinTerminal,
943        deleted_slots: Vec<ObjectSlot>,
944        membership: StoreMembershipStateRef,
945        provider_admin_grant: ProviderAdminGrantId,
946        executor: StoreDeviceRegistrationRef,
947        executor_registration: &StoreDeviceRegistration,
948        executor_signer: &UserKeypair,
949    ) -> Result<Self, DeviceJoinError> {
950        require_cancelled_outcome(&cancellation)?;
951        if cancellation.attempt().attempt_id != attempt.attempt_id {
952            return Err(DeviceJoinError::AttemptMismatch);
953        }
954        executor.verify_registration(executor_registration)?;
955        if executor_registration.store_root != attempt.store_root
956            || keys::public_key_hex(executor_signer) != executor_registration.device_signing_pubkey
957        {
958            return Err(DeviceJoinError::InvalidSignature);
959        }
960        let mut value = Self {
961            version: STORE_PROTOCOL_VERSION,
962            store_root_hash: attempt.store_root.store_root_hash,
963            cancellation,
964            administrator_terminal,
965            joiner_terminal,
966            deleted_slots,
967            membership,
968            provider_admin_grant,
969            executor,
970            signature: String::new(),
971        };
972        value.validate_shape(attempt)?;
973        value.signature = sign(
974            executor_signer,
975            CLEANUP_RECEIPT_DOMAIN,
976            &value.signed_fields(),
977        );
978        Ok(value)
979    }
980
981    pub(in crate::sync::store) fn receipt_hash(&self) -> ObjectHash {
982        ObjectHash::digest(&domain_json(CLEANUP_RECEIPT_DOMAIN, &self.signed_fields()))
983    }
984
985    pub(in crate::sync::store) fn verify(
986        &self,
987        attempt: &DeviceJoinAttempt,
988        executor: &StoreDeviceRegistration,
989    ) -> Result<(), DeviceJoinError> {
990        if self.version != STORE_PROTOCOL_VERSION
991            || self.store_root_hash != attempt.store_root.store_root_hash
992        {
993            return Err(DeviceJoinError::CleanupMismatch);
994        }
995        let mut verified = self.clone();
996        verified.validate_shape(attempt)?;
997        verify_signature(
998            &executor.device_signing_pubkey,
999            &self.signature,
1000            CLEANUP_RECEIPT_DOMAIN,
1001            &self.signed_fields(),
1002        )
1003    }
1004
1005    pub(in crate::sync::store) fn to_bytes(&self) -> Vec<u8> {
1006        serde_json::to_vec(self).expect("device join cleanup receipt serialization cannot fail")
1007    }
1008
1009    fn validate_shape(&mut self, attempt: &DeviceJoinAttempt) -> Result<(), DeviceJoinError> {
1010        validate_terminals(
1011            &self.cancellation,
1012            &self.administrator_terminal,
1013            &self.joiner_terminal,
1014        )?;
1015        let expected = canonical_cleanup_slots(attempt)?;
1016        self.deleted_slots.sort();
1017        if self.deleted_slots != expected {
1018            return Err(DeviceJoinError::CleanupMismatch);
1019        }
1020        Ok(())
1021    }
1022
1023    fn signed_fields(
1024        &self,
1025    ) -> (
1026        u32,
1027        ObjectHash,
1028        &DeviceJoinOutcomeRef,
1029        &ProviderAdminJoinTerminal,
1030        &JoinerJoinTerminal,
1031        &[ObjectSlot],
1032        &StoreMembershipStateRef,
1033        &ProviderAdminGrantId,
1034        &StoreDeviceRegistrationRef,
1035    ) {
1036        (
1037            self.version,
1038            self.store_root_hash,
1039            &self.cancellation,
1040            &self.administrator_terminal,
1041            &self.joiner_terminal,
1042            &self.deleted_slots,
1043            &self.membership,
1044            &self.provider_admin_grant,
1045            &self.executor,
1046        )
1047    }
1048}
1049
1050#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1051#[serde(deny_unknown_fields)]
1052pub struct DeviceJoinCleanupReceipt {
1053    pub receipt: DeviceJoinCleanupReceiptRef,
1054}
1055
1056#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1057#[serde(deny_unknown_fields)]
1058pub struct DeviceJoinCleanupActivation {
1059    pub receipt: DeviceJoinCleanupReceiptRef,
1060    pub activation: StoreBatchCommitRef,
1061}
1062
1063#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1064#[serde(deny_unknown_fields)]
1065pub struct JoinedStore {
1066    pub store_root: StoreRootRef,
1067    pub registration: StoreDeviceRegistrationRef,
1068    pub activation: DeviceJoinActivation,
1069}
1070
1071fn sign<T: Serialize>(signer: &UserKeypair, domain: &[u8], value: &T) -> String {
1072    let digest = ObjectHash::digest(&domain_json(domain, value));
1073    hex::encode(signer.sign(digest.as_bytes()))
1074}
1075
1076fn verify_signature<T: Serialize>(
1077    public_key: &str,
1078    signature: &str,
1079    domain: &[u8],
1080    value: &T,
1081) -> Result<(), DeviceJoinError> {
1082    let digest = ObjectHash::digest(&domain_json(domain, value));
1083    if keys::verify_signature_hex(public_key, signature, digest.as_bytes()) {
1084        Ok(())
1085    } else {
1086        Err(DeviceJoinError::InvalidSignature)
1087    }
1088}
1089
1090fn domain_json<T: Serialize>(domain: &[u8], value: &T) -> Vec<u8> {
1091    let mut bytes = domain.to_vec();
1092    bytes.extend(serde_json::to_vec(value).expect("closed device join serialization cannot fail"));
1093    bytes
1094}