Skip to main content

coven_protocol/store_commit/
device_join_exchange.rs

1use serde::{Deserialize, Serialize};
2
3use crate::membership::MembershipGrantId;
4use crate::objects::{ExactObjectRef, ObjectSlot};
5use crate::provider::{
6    ActivatedStoreMemberProviderAccessGrant, CrossPrincipalProbeChallenge,
7    CrossPrincipalProbeReceipt, CrossPrincipalProbeResponse,
8    DeviceJoinChallengePublicationAuthorization, ProviderAdminGrantRecord,
9};
10use crate::store_commit::{Signed, SignedBody};
11use crate::{ProviderDeviceBinding, StoreProviderBinding};
12use coven_keys::keys::{self, UserKeypair};
13
14use super::device_join::{DeviceJoinAbandonmentRef, DeviceJoinAttemptId};
15use super::{StoreDeviceRegistration, StoreDeviceRegistrationRef, StoreRootRef};
16
17use super::*;
18
19/// A signed join-exchange value that contradicts itself, its signer, or the
20/// exchange it extends. Workflow errors wrap it at the operation boundary.
21#[derive(Debug, thiserror::Error)]
22pub enum DeviceJoinExchangeError {
23    #[error("device join signature is invalid")]
24    InvalidSignature,
25    #[error("device join offer does not name one active Store/member/provider authority")]
26    OfferMismatch,
27    #[error("device provider approval differs from its request or grant")]
28    ApprovalMismatch,
29    #[error("device registration request differs from its offer, approval, or reserved slots")]
30    RegistrationRequestMismatch,
31    #[error("device join attempt differs from its signed exchange")]
32    AttemptMismatch,
33    #[error("device join cleanup does not contain the unconditional canonical slot set")]
34    CleanupMismatch,
35    #[error("device join reserved slots are not distinct")]
36    DuplicateReservedSlot,
37    #[error("provider: {0}")]
38    Provider(#[from] crate::provider::ProviderProbeError),
39    #[error("{0}")]
40    Storage(#[from] crate::objects::StorageError),
41    #[error("{0}")]
42    Protocol(#[from] super::StoreProtocolError),
43}
44
45const OFFER_DOMAIN: &[u8] = b"coven.device-join-offer.v1\0";
46const ACCESS_REQUEST_DOMAIN: &[u8] = b"coven.device-provider-access-request.v1\0";
47const APPROVAL_DOMAIN: &[u8] = b"coven.device-provider-admission-approval.v1\0";
48const REGISTRATION_REQUEST_DOMAIN: &[u8] = b"coven.device-registration-request.v1\0";
49const ABANDONMENT_DOMAIN: &[u8] = b"coven.device-join-abandonment.v1\0";
50
51/// The wire body of a device-join offer. Every field here is signed.
52#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
53#[serde(deny_unknown_fields)]
54pub struct DeviceJoinOfferBody {
55    pub attempt_id: DeviceJoinAttemptId,
56    pub member_pubkey: String,
57    pub store_root: StoreRootRef,
58    pub provider: StoreProviderBinding,
59    pub owner_registration: StoreDeviceRegistrationRef,
60    pub owner_grant: MembershipGrantId,
61    pub provider_admin: Box<ProviderAdminGrantRecord>,
62}
63
64impl SignedBody for DeviceJoinOfferBody {
65    const DOMAIN: &'static [u8] = OFFER_DOMAIN;
66}
67
68pub type DeviceJoinOffer = Signed<DeviceJoinOfferBody>;
69
70impl DeviceJoinOfferBody {
71    fn validate_shape(&self) -> Result<(), DeviceJoinExchangeError> {
72        if self.member_pubkey.is_empty()
73            || self.provider_admin.administrator != self.owner_registration
74        {
75            return Err(DeviceJoinExchangeError::OfferMismatch);
76        }
77        self.provider.validate()?;
78        self.provider_admin
79            .provider
80            .validate_for(&self.provider)
81            .map_err(DeviceJoinExchangeError::Storage)?;
82        if let crate::provider::ProviderAdminGrantOrigin::Founder { root } =
83            &self.provider_admin.created_at
84        {
85            if root != &self.store_root {
86                return Err(DeviceJoinExchangeError::OfferMismatch);
87            }
88        }
89        Ok(())
90    }
91}
92
93impl DeviceJoinOffer {
94    #[allow(clippy::too_many_arguments)]
95    pub fn signed(
96        attempt_id: DeviceJoinAttemptId,
97        member_pubkey: String,
98        store_root: StoreRootRef,
99        provider: StoreProviderBinding,
100        owner_registration: StoreDeviceRegistrationRef,
101        owner_grant: MembershipGrantId,
102        provider_admin: ProviderAdminGrantRecord,
103        owner: &StoreDeviceRegistration,
104        owner_device_signer: &UserKeypair,
105    ) -> Result<Self, DeviceJoinExchangeError> {
106        owner_registration.verify_registration(owner)?;
107        if keys::public_key_hex(owner_device_signer) != owner.device_signing_pubkey {
108            return Err(DeviceJoinExchangeError::InvalidSignature);
109        }
110        let body = DeviceJoinOfferBody {
111            attempt_id,
112            member_pubkey,
113            store_root,
114            provider,
115            owner_registration,
116            owner_grant,
117            provider_admin: Box::new(provider_admin),
118        };
119        body.validate_shape()?;
120        Ok(Signed::sign(body, owner_device_signer))
121    }
122
123    pub fn verify(&self, owner: &StoreDeviceRegistration) -> Result<(), DeviceJoinExchangeError> {
124        self.body().validate_shape()?;
125        self.owner_registration.verify_registration(owner)?;
126        self.verify_by(&owner.device_signing_pubkey)
127            .map_err(|_| DeviceJoinExchangeError::InvalidSignature)
128    }
129
130    pub(crate) fn offer_hash(&self) -> ObjectHash {
131        self.hash()
132    }
133}
134
135/// The wire body of a joining device's provider-access request.
136#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
137#[serde(deny_unknown_fields)]
138pub struct DeviceProviderAccessRequestBody {
139    pub offer: Box<DeviceJoinOffer>,
140    pub peer_provider: ProviderDeviceBinding,
141    pub expected_registration: StoreDeviceRegistration,
142    pub registration_slot: ObjectSlot,
143}
144
145impl SignedBody for DeviceProviderAccessRequestBody {
146    const DOMAIN: &'static [u8] = ACCESS_REQUEST_DOMAIN;
147}
148
149pub type DeviceProviderAccessRequest = Signed<DeviceProviderAccessRequestBody>;
150
151impl DeviceProviderAccessRequest {
152    pub fn signed(
153        offer: DeviceJoinOffer,
154        peer_provider: ProviderDeviceBinding,
155        expected_registration: StoreDeviceRegistration,
156        registration_slot: ObjectSlot,
157        member_signer: &UserKeypair,
158    ) -> Result<Self, DeviceJoinExchangeError> {
159        if keys::public_key_hex(member_signer) != offer.member_pubkey {
160            return Err(DeviceJoinExchangeError::InvalidSignature);
161        }
162        let body = DeviceProviderAccessRequestBody {
163            offer: Box::new(offer),
164            peer_provider,
165            expected_registration,
166            registration_slot,
167        };
168        body.validate_shape()?;
169        Ok(Signed::sign(body, member_signer))
170    }
171
172    pub fn verify(&self, owner: &StoreDeviceRegistration) -> Result<(), DeviceJoinExchangeError> {
173        self.offer.verify(owner)?;
174        self.body().validate_shape()?;
175        self.verify_by(&self.offer.member_pubkey)
176            .map_err(|_| DeviceJoinExchangeError::InvalidSignature)
177    }
178
179    pub fn request_hash(&self) -> ObjectHash {
180        self.hash()
181    }
182
183    pub fn cross_challenge_context(&self) -> crate::provider::CrossPrincipalChallengeContext {
184        crate::provider::CrossPrincipalChallengeContext {
185            root: self.offer.store_root.clone(),
186            attempt_id: self.offer.attempt_id,
187            access_request_hash: self.request_hash(),
188            provider_admin_grant: self.offer.provider_admin.grant_id.clone(),
189            owner_registration: self.offer.owner_registration.clone(),
190            member_pubkey: self.offer.member_pubkey.clone(),
191            administrator_binding: self.offer.provider_admin.provider.clone(),
192            peer_binding: self.peer_provider.clone(),
193        }
194    }
195}
196
197impl DeviceProviderAccessRequestBody {
198    fn validate_shape(&self) -> Result<(), DeviceJoinExchangeError> {
199        let offer = &self.offer;
200        self.peer_provider.validate_for(&offer.provider)?;
201        if self.expected_registration.store_root != offer.store_root
202            || self.expected_registration.author_pubkey != offer.member_pubkey
203            || self.expected_registration.provider != self.peer_provider
204        {
205            return Err(DeviceJoinExchangeError::RegistrationRequestMismatch);
206        }
207        match &self.expected_registration.origin {
208            crate::store_commit::StoreDeviceRegistrationOrigin::Join { attempt_id }
209                if *attempt_id == offer.attempt_id => {}
210            _ => return Err(DeviceJoinExchangeError::RegistrationRequestMismatch),
211        }
212        let slots = vec![
213            self.registration_slot.clone(),
214            self.expected_registration
215                .acknowledgements
216                .first_slot()
217                .clone(),
218        ];
219        require_distinct_slots(&slots)
220    }
221}
222
223#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
224#[serde(rename_all = "snake_case", deny_unknown_fields)]
225pub enum DeviceProviderAdmission {
226    SamePrincipal,
227    CrossPrincipal {
228        access_grant: Box<ActivatedStoreMemberProviderAccessGrant>,
229        challenge: CrossPrincipalProbeChallenge,
230    },
231}
232
233/// The wire body of a provider administrator's admission approval.
234#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
235#[serde(deny_unknown_fields)]
236pub struct DeviceProviderAdmissionApprovalBody {
237    pub request: Box<DeviceProviderAccessRequest>,
238    pub admission: DeviceProviderAdmission,
239}
240
241impl SignedBody for DeviceProviderAdmissionApprovalBody {
242    const DOMAIN: &'static [u8] = APPROVAL_DOMAIN;
243}
244
245pub type DeviceProviderAdmissionApproval = Signed<DeviceProviderAdmissionApprovalBody>;
246
247impl DeviceProviderAdmissionApprovalBody {
248    fn validate_shape(
249        &self,
250        store_root: &crate::objects::VerifiedObject<StoreProtocolRoot>,
251        owner: &StoreDeviceRegistration,
252    ) -> Result<(), DeviceJoinExchangeError> {
253        let offer = &self.request.offer;
254        if store_root.object != offer.store_root.object
255            || store_root.value.object_hash() != offer.store_root.store_root_hash
256            || store_root.value.descriptor.store_root_id() != offer.store_root.store_root_id
257            || store_root.value.descriptor.provider != offer.provider
258        {
259            return Err(DeviceJoinExchangeError::ApprovalMismatch);
260        }
261        let same_principal = offer.provider_admin.provider == self.request.peer_provider;
262        match &self.admission {
263            DeviceProviderAdmission::SamePrincipal if same_principal => {}
264            DeviceProviderAdmission::CrossPrincipal { access_grant, .. }
265                if !same_principal
266                    && access_grant.grant.member_pubkey == offer.member_pubkey
267                    && access_grant.grant.provider == self.request.peer_provider
268                    && access_grant.grant_ref.grant_id == access_grant.grant.grant_id
269                    && access_grant.grant_ref.grant_hash == access_grant.grant.grant_hash()
270                    && access_grant.grant.administrator_grant == offer.provider_admin.grant_id
271                    && access_grant.grant.administrator == offer.provider_admin.administrator =>
272            {
273                access_grant.grant.verify(&offer.provider, owner)?;
274            }
275            _ => return Err(DeviceJoinExchangeError::ApprovalMismatch),
276        }
277        Ok(())
278    }
279}
280
281impl DeviceProviderAdmissionApproval {
282    pub fn access_grant(&self) -> Option<&ActivatedStoreMemberProviderAccessGrant> {
283        match &self.admission {
284            DeviceProviderAdmission::SamePrincipal => None,
285            DeviceProviderAdmission::CrossPrincipal { access_grant, .. } => Some(access_grant),
286        }
287    }
288
289    pub fn signed(
290        request: DeviceProviderAccessRequest,
291        admission: DeviceProviderAdmission,
292        store_root: &crate::objects::VerifiedObject<StoreProtocolRoot>,
293        owner: &StoreDeviceRegistration,
294        owner_device_signer: &UserKeypair,
295    ) -> Result<Self, DeviceJoinExchangeError> {
296        if keys::public_key_hex(owner_device_signer) != owner.device_signing_pubkey {
297            return Err(DeviceJoinExchangeError::InvalidSignature);
298        }
299        let body = DeviceProviderAdmissionApprovalBody {
300            request: Box::new(request),
301            admission,
302        };
303        body.validate_shape(store_root, owner)?;
304        Ok(Signed::sign(body, owner_device_signer))
305    }
306
307    /// One registration answers the whole approval: the device that signed the
308    /// offer is the device that holds the store's provider-administrator grant,
309    /// so it is also the signer of this approval and of the access grant inside
310    /// it.
311    pub fn verify(
312        &self,
313        store_root: &crate::objects::VerifiedObject<StoreProtocolRoot>,
314        owner: &StoreDeviceRegistration,
315    ) -> Result<(), DeviceJoinExchangeError> {
316        self.request.verify(owner)?;
317        self.body().validate_shape(store_root, owner)?;
318        self.verify_by(&owner.device_signing_pubkey)
319            .map_err(|_| DeviceJoinExchangeError::InvalidSignature)
320    }
321
322    #[cfg(any(test, feature = "test-utils"))]
323    pub fn signed_without_shape_validation_for_test(
324        request: DeviceProviderAccessRequest,
325        admission: DeviceProviderAdmission,
326        owner_device_signer: &UserKeypair,
327    ) -> Self {
328        Signed::sign(
329            DeviceProviderAdmissionApprovalBody {
330                request: Box::new(request),
331                admission,
332            },
333            owner_device_signer,
334        )
335    }
336}
337
338#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
339#[serde(rename_all = "snake_case", deny_unknown_fields)]
340pub enum DeviceProviderResponseReservation {
341    SamePrincipal,
342    CrossPrincipal { response_slot: ObjectSlot },
343}
344
345#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
346#[serde(deny_unknown_fields)]
347pub struct CrossPrincipalDeviceRegistrationRequestBody {
348    pub approval: Box<DeviceProviderAdmissionApproval>,
349    pub response_slot: ObjectSlot,
350}
351
352impl SignedBody for CrossPrincipalDeviceRegistrationRequestBody {
353    const DOMAIN: &'static [u8] = REGISTRATION_REQUEST_DOMAIN;
354}
355
356pub type CrossPrincipalDeviceRegistrationRequest =
357    Signed<CrossPrincipalDeviceRegistrationRequestBody>;
358
359/// A same-provider registration needs no second signature: the joining
360/// device's access request already signed the complete registration. A
361/// cross-provider registration additionally signs the response slot allocated
362/// after the administrator publishes its challenge.
363#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
364#[serde(rename_all = "snake_case", deny_unknown_fields)]
365pub enum DeviceRegistrationRequest {
366    SamePrincipal {
367        approval: Box<DeviceProviderAdmissionApproval>,
368    },
369    CrossPrincipal(CrossPrincipalDeviceRegistrationRequest),
370}
371
372impl DeviceRegistrationRequest {
373    pub fn same_principal(
374        approval: DeviceProviderAdmissionApproval,
375    ) -> Result<Self, DeviceJoinExchangeError> {
376        let request = Self::SamePrincipal {
377            approval: Box::new(approval),
378        };
379        request.verify()?;
380        Ok(request)
381    }
382
383    pub fn cross_principal(
384        approval: DeviceProviderAdmissionApproval,
385        response_slot: ObjectSlot,
386        member_signer: &UserKeypair,
387    ) -> Result<Self, DeviceJoinExchangeError> {
388        if keys::public_key_hex(member_signer) != approval.request.offer.member_pubkey {
389            return Err(DeviceJoinExchangeError::InvalidSignature);
390        }
391        let signed = Signed::sign(
392            CrossPrincipalDeviceRegistrationRequestBody {
393                approval: Box::new(approval),
394                response_slot,
395            },
396            member_signer,
397        );
398        let request = Self::CrossPrincipal(signed);
399        request.verify()?;
400        Ok(request)
401    }
402
403    pub fn verify(&self) -> Result<(), DeviceJoinExchangeError> {
404        self.approval().request.body().validate_shape()?;
405        match self {
406            Self::SamePrincipal { approval }
407                if matches!(approval.admission, DeviceProviderAdmission::SamePrincipal) =>
408            {
409                Ok(())
410            }
411            Self::CrossPrincipal(request)
412                if matches!(
413                    request.approval.admission,
414                    DeviceProviderAdmission::CrossPrincipal { .. }
415                ) =>
416            {
417                let member_pubkey = request.approval.request.offer.member_pubkey.clone();
418                request
419                    .verify_by(&member_pubkey)
420                    .map_err(|_| DeviceJoinExchangeError::InvalidSignature)?;
421                let mut slots = vec![
422                    request.approval.request.registration_slot.clone(),
423                    request
424                        .approval
425                        .request
426                        .expected_registration
427                        .acknowledgements
428                        .first_slot()
429                        .clone(),
430                    request.response_slot.clone(),
431                ];
432                if let DeviceProviderAdmission::CrossPrincipal { challenge, .. } =
433                    &request.approval.admission
434                {
435                    slots.push(challenge.administrator_object.slot.clone());
436                }
437                require_distinct_slots(&slots)
438            }
439            _ => Err(DeviceJoinExchangeError::RegistrationRequestMismatch),
440        }
441    }
442
443    pub fn approval(&self) -> &DeviceProviderAdmissionApproval {
444        match self {
445            Self::SamePrincipal { approval } => approval,
446            Self::CrossPrincipal(request) => &request.approval,
447        }
448    }
449
450    pub fn expected_registration(&self) -> &StoreDeviceRegistration {
451        &self.approval().request.expected_registration
452    }
453
454    pub fn registration_slot(&self) -> &ObjectSlot {
455        &self.approval().request.registration_slot
456    }
457
458    pub fn response(&self) -> DeviceProviderResponseReservation {
459        match self {
460            Self::SamePrincipal { .. } => DeviceProviderResponseReservation::SamePrincipal,
461            Self::CrossPrincipal(request) => DeviceProviderResponseReservation::CrossPrincipal {
462                response_slot: request.response_slot.clone(),
463            },
464        }
465    }
466}
467
468#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
469#[serde(deny_unknown_fields)]
470pub struct ProvisionalDeviceBootstrap {
471    pub request: Box<DeviceRegistrationRequest>,
472    pub publication_authorization: DeviceJoinChallengePublicationAuthorization,
473}
474
475#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
476#[serde(rename_all = "snake_case", deny_unknown_fields)]
477pub enum DeviceProviderChallengePublication {
478    SamePrincipal,
479    CrossPrincipal {
480        challenge: CrossPrincipalProbeChallenge,
481    },
482}
483
484#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
485#[serde(deny_unknown_fields)]
486pub struct ProviderReadyDeviceBootstrap {
487    pub bootstrap: Box<ProvisionalDeviceBootstrap>,
488    pub challenge_publication: DeviceProviderChallengePublication,
489}
490
491#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
492#[serde(rename_all = "snake_case", deny_unknown_fields)]
493pub enum DeviceProviderReadiness {
494    SamePrincipal,
495    CrossPrincipal(CrossPrincipalProbeResponse),
496}
497
498#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
499#[serde(deny_unknown_fields)]
500pub struct DeviceJoinReadiness {
501    pub proof: DeviceReadinessProof,
502    pub provider: DeviceProviderReadiness,
503}
504
505#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
506#[serde(rename_all = "snake_case", deny_unknown_fields)]
507pub enum DeviceProviderAdmissionCompletion {
508    SamePrincipal {
509        bootstrap: Box<ProviderReadyDeviceBootstrap>,
510    },
511    CrossPrincipal {
512        /// The bootstrap this completion answers. It carries the joining
513        /// device's signed registration request, which is where the expected
514        /// registration and its reserved slot come from now that no separate
515        /// attempt file restates them.
516        bootstrap: Box<ProviderReadyDeviceBootstrap>,
517        readiness: Box<DeviceJoinReadiness>,
518        receipt: CrossPrincipalProbeReceipt,
519    },
520}
521
522impl DeviceProviderAdmissionCompletion {
523    pub fn attempt_id(&self) -> DeviceJoinAttemptId {
524        match self {
525            Self::SamePrincipal { bootstrap } | Self::CrossPrincipal { bootstrap, .. } => {
526                bootstrap.bootstrap.publication_authorization.attempt_id
527            }
528        }
529    }
530
531    /// The bootstrap this completion answers, whichever provider shape it took.
532    pub fn bootstrap(&self) -> &ProviderReadyDeviceBootstrap {
533        match self {
534            Self::SamePrincipal { bootstrap } | Self::CrossPrincipal { bootstrap, .. } => bootstrap,
535        }
536    }
537}
538
539#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
540#[serde(deny_unknown_fields)]
541pub struct DeviceJoinActivation {
542    pub attempt_id: DeviceJoinAttemptId,
543    pub outcome_activation: StoreBatchCommitRef,
544}
545
546/// The canonical evidence for one Merge commit required to install a device
547/// join. Every value is re-verified against its exact reference before the
548/// joining database accepts it.
549#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
550#[serde(deny_unknown_fields)]
551pub struct DeviceJoinBootstrapCommitClosure {
552    pub reference: StoreBatchCommitRef,
553    pub canonical_commit: Vec<u8>,
554    pub author: ReferencedStoreDeviceRegistration,
555    pub registrations: RetainedStoreDeviceRegistrationActivations,
556    pub device_operations: RetainedStoreDeviceOperations,
557    pub activation_head: StoreDeviceHead,
558    pub activation_object: ExactObjectRef,
559    pub history_evidence: RetainedMergeCommitEvidence,
560}
561
562/// The exact verified history required after the selected snapshot. This is a
563/// transfer representation; the joining database reconstructs its verified
564/// bootstrap plan before mutating any Store state.
565#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
566#[serde(deny_unknown_fields)]
567pub struct DeviceJoinBootstrapClosure {
568    pub founder: ReferencedStoreDeviceRegistration,
569    pub genesis: ResolvedStoreDeviceState,
570    pub membership: crate::membership::MembershipFloor,
571    pub commits: Vec<DeviceJoinBootstrapCommitClosure>,
572}
573
574/// The signed snapshot authority and exact Merge closure a same-provider
575/// joining device needs. The snapshot image remains in provider storage and is
576/// the only Store object downloaded after this response arrives.
577#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
578#[serde(deny_unknown_fields)]
579pub struct SamePrincipalStoreInstallation {
580    pub store_root: StoreProtocolRoot,
581    pub snapshot: StoreSnapshotRef,
582    pub metadata: SnapshotMeta,
583    pub authority: RetainedReplaySnapshotAuthority,
584    pub bootstrap: DeviceJoinBootstrapClosure,
585}
586
587/// The complete response when the Store and joining device use the same
588/// provider principal. The one activation commit both publishes the attempt
589/// bootstrap and activates the joining registration, so the joiner can install
590/// that exact history and finish without a second transport wait or catch-up.
591#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
592#[serde(deny_unknown_fields)]
593pub struct SamePrincipalDeviceJoin {
594    pub bootstrap: ProviderReadyDeviceBootstrap,
595    pub activation: DeviceJoinActivation,
596    pub installation: Box<SamePrincipalStoreInstallation>,
597}
598
599impl SamePrincipalDeviceJoin {
600    pub fn verified(
601        bootstrap: ProviderReadyDeviceBootstrap,
602        activation: DeviceJoinActivation,
603        installation: SamePrincipalStoreInstallation,
604    ) -> Result<Self, DeviceJoinExchangeError> {
605        let join = Self {
606            bootstrap,
607            activation,
608            installation: Box::new(installation),
609        };
610        join.verify_shape()?;
611        Ok(join)
612    }
613
614    /// Check the parts of a same-provider join against each other.
615    ///
616    /// What this can settle is agreement between the pieces the joining device
617    /// already holds: the request it signed, the authorization the admitting
618    /// device signed over it, and the snapshot authority. That the joining
619    /// device was really registered is not settled here and never was — it is
620    /// settled by the bootstrap closure, whose activation commit names the
621    /// registration and is verified commit by commit against the Store's own
622    /// history.
623    pub fn verify_shape(&self) -> Result<(), DeviceJoinExchangeError> {
624        let bootstrap = &self.bootstrap;
625        let activation = &self.activation;
626        let installation = &self.installation;
627        bootstrap.bootstrap.request.verify()?;
628        let authorization = &bootstrap.bootstrap.publication_authorization;
629        if !matches!(
630            bootstrap.challenge_publication,
631            DeviceProviderChallengePublication::SamePrincipal
632        ) || activation.attempt_id != authorization.attempt_id
633            || activation.outcome_activation != authorization.attempt_activation
634            || installation.authority.store_root
635                != bootstrap
636                    .bootstrap
637                    .request
638                    .approval()
639                    .request
640                    .offer
641                    .store_root
642            || installation.snapshot != installation.authority.snapshot
643            || installation.metadata != installation.authority.metadata
644            || installation.store_root.descriptor.store_root_id()
645                != installation.authority.store_root.store_root_id
646            || installation.store_root.object_hash()
647                != installation.authority.store_root.store_root_hash
648        {
649            return Err(DeviceJoinExchangeError::AttemptMismatch);
650        }
651        Ok(())
652    }
653}
654
655/// The wire body of an owner's abandonment of a join attempt.
656#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
657#[serde(deny_unknown_fields)]
658pub struct DeviceJoinAbandonmentBody {
659    pub store_root_hash: ObjectHash,
660    pub offer_hash: ObjectHash,
661    pub attempt_id: DeviceJoinAttemptId,
662    pub owner_registration: StoreDeviceRegistrationRef,
663    pub owner_grant: MembershipGrantId,
664}
665
666impl SignedBody for DeviceJoinAbandonmentBody {
667    const DOMAIN: &'static [u8] = ABANDONMENT_DOMAIN;
668}
669
670pub type DeviceJoinAbandonmentObject = Signed<DeviceJoinAbandonmentBody>;
671
672impl DeviceJoinAbandonmentObject {
673    pub fn signed(
674        offer: &DeviceJoinOffer,
675        owner: &StoreDeviceRegistration,
676        owner_device_signer: &UserKeypair,
677    ) -> Result<Self, DeviceJoinExchangeError> {
678        offer.verify(owner)?;
679        if keys::public_key_hex(owner_device_signer) != owner.device_signing_pubkey {
680            return Err(DeviceJoinExchangeError::InvalidSignature);
681        }
682        Ok(Signed::sign(
683            DeviceJoinAbandonmentBody {
684                store_root_hash: offer.store_root.store_root_hash,
685                offer_hash: offer.offer_hash(),
686                attempt_id: offer.attempt_id,
687                owner_registration: offer.owner_registration.clone(),
688                owner_grant: offer.owner_grant.clone(),
689            },
690            owner_device_signer,
691        ))
692    }
693
694    pub fn abandonment_hash(&self) -> ObjectHash {
695        self.hash()
696    }
697}
698
699#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
700#[serde(deny_unknown_fields)]
701pub struct DeviceJoinAbandonment {
702    pub abandonment: DeviceJoinAbandonmentRef,
703    pub abandonment_activation: StoreBatchCommitRef,
704}
705
706#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
707#[serde(deny_unknown_fields)]
708pub struct JoinedStore {
709    pub store_root: StoreRootRef,
710    pub registration: StoreDeviceRegistrationRef,
711    pub activation: DeviceJoinActivation,
712}
713
714impl DeviceJoinAbandonmentRef {
715    pub fn verify(
716        &self,
717        abandonment: &DeviceJoinAbandonmentObject,
718        owner: &StoreDeviceRegistration,
719    ) -> Result<(), DeviceJoinExchangeError> {
720        abandonment.owner_registration.verify_registration(owner)?;
721        if self.attempt_id != abandonment.attempt_id
722            || self.abandonment_hash != abandonment.abandonment_hash()
723        {
724            return Err(DeviceJoinExchangeError::AttemptMismatch);
725        }
726        abandonment
727            .verify_by(&owner.device_signing_pubkey)
728            .map_err(|_| DeviceJoinExchangeError::InvalidSignature)
729    }
730}
731
732pub(crate) fn require_distinct_slots(
733    slots: &[crate::objects::ObjectSlot],
734) -> Result<(), DeviceJoinExchangeError> {
735    let unique = slots.iter().collect::<std::collections::BTreeSet<_>>();
736    if unique.len() == slots.len() {
737        Ok(())
738    } else {
739        Err(DeviceJoinExchangeError::DuplicateReservedSlot)
740    }
741}