Skip to main content

coven_protocol/store_commit/
device_join_exchange.rs

1use serde::{Deserialize, Serialize};
2
3use crate::membership::MembershipGrantId;
4use crate::objects::{ObjectSlot, PreparedExactObject};
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                    slots.push(challenge.conditional_slot.clone());
437                }
438                require_distinct_slots(&slots)
439            }
440            _ => Err(DeviceJoinExchangeError::RegistrationRequestMismatch),
441        }
442    }
443
444    pub fn approval(&self) -> &DeviceProviderAdmissionApproval {
445        match self {
446            Self::SamePrincipal { approval } => approval,
447            Self::CrossPrincipal(request) => &request.approval,
448        }
449    }
450
451    pub fn expected_registration(&self) -> &StoreDeviceRegistration {
452        &self.approval().request.expected_registration
453    }
454
455    pub fn registration_slot(&self) -> &ObjectSlot {
456        &self.approval().request.registration_slot
457    }
458
459    pub fn response(&self) -> DeviceProviderResponseReservation {
460        match self {
461            Self::SamePrincipal { .. } => DeviceProviderResponseReservation::SamePrincipal,
462            Self::CrossPrincipal(request) => DeviceProviderResponseReservation::CrossPrincipal {
463                response_slot: request.response_slot.clone(),
464            },
465        }
466    }
467}
468
469#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
470#[serde(deny_unknown_fields)]
471pub struct ProvisionalDeviceBootstrap {
472    pub request: Box<DeviceRegistrationRequest>,
473    pub publication_authorization: DeviceJoinChallengePublicationAuthorization,
474}
475
476#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
477#[serde(rename_all = "snake_case", deny_unknown_fields)]
478pub enum DeviceProviderChallengePublication {
479    SamePrincipal,
480    CrossPrincipal {
481        challenge: CrossPrincipalProbeChallenge,
482    },
483}
484
485#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
486#[serde(deny_unknown_fields)]
487pub struct ProviderReadyDeviceBootstrap {
488    pub bootstrap: Box<ProvisionalDeviceBootstrap>,
489    pub challenge_publication: DeviceProviderChallengePublication,
490}
491
492#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
493#[serde(rename_all = "snake_case", deny_unknown_fields)]
494pub enum DeviceProviderReadiness {
495    SamePrincipal,
496    CrossPrincipal(CrossPrincipalProbeResponse),
497}
498
499#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
500#[serde(deny_unknown_fields)]
501pub struct DeviceJoinReadiness {
502    pub proof: DeviceReadinessProof,
503    pub provider: DeviceProviderReadiness,
504}
505
506#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
507#[serde(rename_all = "snake_case", deny_unknown_fields)]
508pub enum DeviceProviderAdmissionCompletion {
509    SamePrincipal {
510        bootstrap: Box<ProviderReadyDeviceBootstrap>,
511    },
512    CrossPrincipal {
513        /// The bootstrap this completion answers. It carries the joining
514        /// device's signed registration request, which is where the expected
515        /// registration and its reserved slot come from now that no separate
516        /// attempt file restates them.
517        bootstrap: Box<ProviderReadyDeviceBootstrap>,
518        readiness: Box<DeviceJoinReadiness>,
519        receipt: CrossPrincipalProbeReceipt,
520    },
521}
522
523impl DeviceProviderAdmissionCompletion {
524    pub fn attempt_id(&self) -> DeviceJoinAttemptId {
525        match self {
526            Self::SamePrincipal { bootstrap } | Self::CrossPrincipal { bootstrap, .. } => {
527                bootstrap.bootstrap.publication_authorization.attempt_id
528            }
529        }
530    }
531
532    /// The bootstrap this completion answers, whichever provider shape it took.
533    pub fn bootstrap(&self) -> &ProviderReadyDeviceBootstrap {
534        match self {
535            Self::SamePrincipal { bootstrap } | Self::CrossPrincipal { bootstrap, .. } => bootstrap,
536        }
537    }
538}
539
540#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
541#[serde(deny_unknown_fields)]
542pub struct DeviceJoinActivation {
543    pub attempt_id: DeviceJoinAttemptId,
544    pub outcome_activation: StoreBatchCommitRef,
545}
546
547/// The canonical evidence for one Merge commit required to install a device
548/// join. Every value is re-verified against its exact reference before the
549/// joining database accepts it.
550#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
551#[serde(deny_unknown_fields)]
552pub struct DeviceJoinBootstrapCommitClosure {
553    pub reference: StoreBatchCommitRef,
554    pub canonical_commit: Vec<u8>,
555    pub author: ReferencedStoreDeviceRegistration,
556    pub registrations: RetainedStoreDeviceRegistrationActivations,
557    pub device_operations: RetainedStoreDeviceOperations,
558    pub history_evidence: RetainedMergeCommitEvidence,
559}
560
561#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
562#[serde(deny_unknown_fields)]
563pub struct DeviceJoinBootstrapPublicationEntry {
564    pub entry: PreparedExactObject,
565    pub author: ReferencedStoreDeviceRegistration,
566}
567
568#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
569#[serde(deny_unknown_fields)]
570pub struct DeviceJoinBootstrapPublicationInterval {
571    pub previous: StoreCurrentPublicationRecordBody,
572    pub current: StoreCurrentPublicationRecord,
573    pub entries: Vec<DeviceJoinBootstrapPublicationEntry>,
574}
575
576impl DeviceJoinBootstrapPublicationInterval {
577    pub fn verify(&self) -> Result<VerifiedStorePublicationInterval, StoreProtocolError> {
578        let entries = self
579            .entries
580            .iter()
581            .map(|carried| {
582                let entry: StorePublicationEntry =
583                    crate::objects::decode_protocol_object(carried.entry.stored_bytes())?;
584                let reference =
585                    StorePublicationRef::from_entry(&entry, carried.entry.reference().clone())?;
586                Ok(StorePublicationIntervalEntry::new(
587                    entry,
588                    reference,
589                    carried.author.clone(),
590                ))
591            })
592            .collect::<Result<Vec<_>, StoreProtocolError>>()?;
593        VerifiedStorePublicationInterval::from_nonempty_history(
594            self.previous.clone(),
595            self.current.clone(),
596            entries,
597        )
598    }
599}
600
601/// The exact verified history required after the selected snapshot. This is a
602/// transfer representation; the joining database reconstructs its verified
603/// bootstrap plan before mutating any Store state.
604#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
605#[serde(deny_unknown_fields)]
606pub struct DeviceJoinBootstrapClosure {
607    pub founder: ReferencedStoreDeviceRegistration,
608    pub genesis: ResolvedStoreDeviceState,
609    pub membership: crate::membership::MembershipFloor,
610    pub publication: DeviceJoinBootstrapPublicationInterval,
611    pub commits: Vec<DeviceJoinBootstrapCommitClosure>,
612}
613
614impl DeviceJoinBootstrapClosure {
615    pub fn verified_commit(
616        &self,
617        reference: &StoreBatchCommitRef,
618    ) -> Result<VerifiedStoreBatchCommit, StoreProtocolError> {
619        let carried = self
620            .commits
621            .iter()
622            .find(|carried| &carried.reference == reference)
623            .ok_or_else(|| {
624                StoreProtocolError::Malformed(
625                    "retained device join omits the requested exact commit".into(),
626                )
627            })?;
628        let commit = VerifiedStoreBatchCommit::parse(
629            &carried.canonical_commit,
630            self.publication.current.store_root_hash,
631            reference,
632            carried.author.value(),
633        )?;
634        if commit.author_registration != *carried.author.reference() {
635            return Err(StoreProtocolError::DeviceStateMismatch);
636        }
637        Ok(commit)
638    }
639
640    pub fn accepted_commit(
641        &self,
642        reference: &StoreBatchCommitRef,
643    ) -> Result<AcceptedStoreCommitPublication, StoreProtocolError> {
644        self.publication
645            .verify()?
646            .accepted_commit(&self.verified_commit(reference)?)
647    }
648}
649
650/// The signed snapshot authority and exact Merge closure a same-provider
651/// joining device needs. The snapshot image remains in provider storage and is
652/// the only Store object downloaded after this response arrives.
653#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
654#[serde(deny_unknown_fields)]
655pub struct SamePrincipalStoreInstallation {
656    pub store_root: StoreProtocolRoot,
657    pub authority: RetainedReplaySnapshotAuthority,
658    pub bootstrap: DeviceJoinBootstrapClosure,
659}
660
661/// The complete response when the Store and joining device use the same
662/// provider principal. The one activation commit both publishes the attempt
663/// bootstrap and activates the joining registration, so the joiner can install
664/// that exact history and finish without a second transport wait or catch-up.
665#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
666#[serde(deny_unknown_fields)]
667pub struct SamePrincipalDeviceJoin {
668    pub bootstrap: ProviderReadyDeviceBootstrap,
669    pub activation: DeviceJoinActivation,
670    pub installation: Box<SamePrincipalStoreInstallation>,
671}
672
673impl SamePrincipalDeviceJoin {
674    pub fn verified(
675        bootstrap: ProviderReadyDeviceBootstrap,
676        activation: DeviceJoinActivation,
677        installation: SamePrincipalStoreInstallation,
678    ) -> Result<Self, DeviceJoinExchangeError> {
679        let join = Self {
680            bootstrap,
681            activation,
682            installation: Box::new(installation),
683        };
684        join.verify_shape()?;
685        Ok(join)
686    }
687
688    /// Check the parts of a same-provider join against each other.
689    ///
690    /// What this can settle is agreement between the pieces the joining device
691    /// already holds: the request it signed, the authorization the admitting
692    /// device signed over it, and the snapshot authority. That the joining
693    /// device was really registered is not settled here and never was — it is
694    /// settled by the bootstrap closure, whose activation commit names the
695    /// registration and is verified commit by commit against the Store's own
696    /// history.
697    pub fn verify_shape(&self) -> Result<(), DeviceJoinExchangeError> {
698        let bootstrap = &self.bootstrap;
699        let activation = &self.activation;
700        let installation = &self.installation;
701        bootstrap.bootstrap.request.verify()?;
702        let authorization = &bootstrap.bootstrap.publication_authorization;
703        if !matches!(
704            bootstrap.challenge_publication,
705            DeviceProviderChallengePublication::SamePrincipal
706        ) || activation.attempt_id != authorization.attempt_id
707            || activation.outcome_activation != authorization.attempt_activation
708            || installation.authority.store_root
709                != bootstrap
710                    .bootstrap
711                    .request
712                    .approval()
713                    .request
714                    .offer
715                    .store_root
716            || installation.store_root.descriptor.store_root_id()
717                != installation.authority.store_root.store_root_id
718            || installation.store_root.object_hash()
719                != installation.authority.store_root.store_root_hash
720        {
721            return Err(DeviceJoinExchangeError::AttemptMismatch);
722        }
723        Ok(())
724    }
725}
726
727/// The wire body of an owner's abandonment of a join attempt.
728#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
729#[serde(deny_unknown_fields)]
730pub struct DeviceJoinAbandonmentBody {
731    pub store_root_hash: ObjectHash,
732    pub offer_hash: ObjectHash,
733    pub attempt_id: DeviceJoinAttemptId,
734    pub owner_registration: StoreDeviceRegistrationRef,
735    pub owner_grant: MembershipGrantId,
736}
737
738impl SignedBody for DeviceJoinAbandonmentBody {
739    const DOMAIN: &'static [u8] = ABANDONMENT_DOMAIN;
740}
741
742pub type DeviceJoinAbandonmentObject = Signed<DeviceJoinAbandonmentBody>;
743
744impl DeviceJoinAbandonmentObject {
745    pub fn signed(
746        offer: &DeviceJoinOffer,
747        owner: &StoreDeviceRegistration,
748        owner_device_signer: &UserKeypair,
749    ) -> Result<Self, DeviceJoinExchangeError> {
750        offer.verify(owner)?;
751        if keys::public_key_hex(owner_device_signer) != owner.device_signing_pubkey {
752            return Err(DeviceJoinExchangeError::InvalidSignature);
753        }
754        Ok(Signed::sign(
755            DeviceJoinAbandonmentBody {
756                store_root_hash: offer.store_root.store_root_hash,
757                offer_hash: offer.offer_hash(),
758                attempt_id: offer.attempt_id,
759                owner_registration: offer.owner_registration.clone(),
760                owner_grant: offer.owner_grant.clone(),
761            },
762            owner_device_signer,
763        ))
764    }
765
766    pub fn abandonment_hash(&self) -> ObjectHash {
767        self.hash()
768    }
769}
770
771#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
772#[serde(deny_unknown_fields)]
773pub struct DeviceJoinAbandonment {
774    pub abandonment: DeviceJoinAbandonmentRef,
775    pub abandonment_activation: StoreBatchCommitRef,
776}
777
778#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
779#[serde(deny_unknown_fields)]
780pub struct JoinedStore {
781    pub store_root: StoreRootRef,
782    pub registration: StoreDeviceRegistrationRef,
783    pub activation: DeviceJoinActivation,
784}
785
786impl DeviceJoinAbandonmentRef {
787    pub fn verify(
788        &self,
789        abandonment: &DeviceJoinAbandonmentObject,
790        owner: &StoreDeviceRegistration,
791    ) -> Result<(), DeviceJoinExchangeError> {
792        abandonment.owner_registration.verify_registration(owner)?;
793        if self.attempt_id != abandonment.attempt_id
794            || self.abandonment_hash != abandonment.abandonment_hash()
795        {
796            return Err(DeviceJoinExchangeError::AttemptMismatch);
797        }
798        abandonment
799            .verify_by(&owner.device_signing_pubkey)
800            .map_err(|_| DeviceJoinExchangeError::InvalidSignature)
801    }
802}
803
804pub(crate) fn require_distinct_slots(
805    slots: &[crate::objects::ObjectSlot],
806) -> Result<(), DeviceJoinExchangeError> {
807    let unique = slots.iter().collect::<std::collections::BTreeSet<_>>();
808    if unique.len() == slots.len() {
809        Ok(())
810    } else {
811        Err(DeviceJoinExchangeError::DuplicateReservedSlot)
812    }
813}