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#[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#[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#[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#[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 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#[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 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 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#[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#[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#[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#[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 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#[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}