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