1use std::collections::BTreeSet;
9use std::path::{Path, PathBuf};
10
11use rusqlite::{Connection, OptionalExtension};
12use serde::{Deserialize, Serialize};
13
14use crate::database::Database;
15use crate::keys::{self, UserKeypair};
16use crate::storage::cloud::{ExactSlotStorage, ObjectSlot};
17use crate::sync::circle_control::StoreMembershipStateRef;
18use crate::sync::membership::{MemberRole, MembershipChain, MembershipGrantId};
19use crate::sync::provider::{
20 ActivatedStoreMemberProviderAccessGrant, CrossPrincipalProbeChallenge,
21 CrossPrincipalProbeReceipt, CrossPrincipalProbeResponse,
22 DeviceJoinChallengePublicationAuthorization, ProviderAccessGrantId, ProviderAccessWithdrawal,
23 ProviderAdminGrantId, ProviderAdminGrantRecord, StoreMemberProviderAccessGrant,
24 StoreMemberProviderAccessGrantRef,
25};
26use crate::sync::storage::{
27 CoordinationStorage, ExactObjectRef, ProviderDeviceBinding, StoreProviderBinding, SyncStorage,
28};
29use crate::sync::store_commit::{
30 DeviceJoinAttempt, DeviceJoinAttemptId, DeviceJoinAttemptRef, DeviceJoinOutcomeRef,
31 DeviceReadinessProof, ObjectHash, StoreBatchCommitRef, StoreDeviceRegistration,
32 StoreDeviceRegistrationRef, StoreRootRef, STORE_PROTOCOL_VERSION,
33};
34
35const OFFER_DOMAIN: &[u8] = b"coven.device-join-offer.v1\0";
36const ACCESS_REQUEST_DOMAIN: &[u8] = b"coven.device-provider-access-request.v1\0";
37const APPROVAL_DOMAIN: &[u8] = b"coven.device-provider-admission-approval.v1\0";
38const REGISTRATION_REQUEST_DOMAIN: &[u8] = b"coven.device-registration-request.v1\0";
39const ABANDONMENT_DOMAIN: &[u8] = b"coven.device-join-abandonment.v1\0";
40const PROVIDER_CLOSURE_DOMAIN: &[u8] = b"coven.device-join-provider-closure.v1\0";
41const JOINER_CLOSURE_DOMAIN: &[u8] = b"coven.device-join-joiner-closure.v1\0";
42const WRITE_REVOCATION_DOMAIN: &[u8] = b"coven.device-join-write-revocation.v1\0";
43const CLEANUP_RECEIPT_DOMAIN: &[u8] = b"coven.device-join-cleanup-receipt.v1\0";
44
45#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
46#[serde(deny_unknown_fields)]
47pub struct DeviceJoinOffer {
48 pub version: u32,
49 pub attempt_id: DeviceJoinAttemptId,
50 pub member_pubkey: String,
51 pub store_root: StoreRootRef,
52 pub provider: StoreProviderBinding,
53 pub attempt_slot: ObjectSlot,
54 pub outcome_slot: ObjectSlot,
55 pub owner_registration: StoreDeviceRegistrationRef,
56 pub owner_grant: MembershipGrantId,
57 pub provider_admin: Box<ProviderAdminGrantRecord>,
58 pub signature: String,
59}
60
61impl DeviceJoinOffer {
62 #[allow(clippy::too_many_arguments)]
63 pub fn signed(
64 attempt_id: DeviceJoinAttemptId,
65 member_pubkey: String,
66 store_root: StoreRootRef,
67 provider: StoreProviderBinding,
68 attempt_slot: ObjectSlot,
69 outcome_slot: ObjectSlot,
70 owner_registration: StoreDeviceRegistrationRef,
71 owner_grant: MembershipGrantId,
72 provider_admin: ProviderAdminGrantRecord,
73 owner: &StoreDeviceRegistration,
74 owner_device_signer: &UserKeypair,
75 ) -> Result<Self, DeviceJoinError> {
76 owner_registration.verify_registration(owner)?;
77 if keys::public_key_hex(owner_device_signer) != owner.device_signing_pubkey {
78 return Err(DeviceJoinError::InvalidSignature);
79 }
80 let mut value = Self {
81 version: STORE_PROTOCOL_VERSION,
82 attempt_id,
83 member_pubkey,
84 store_root,
85 provider,
86 attempt_slot,
87 outcome_slot,
88 owner_registration,
89 owner_grant,
90 provider_admin: Box::new(provider_admin),
91 signature: String::new(),
92 };
93 value.validate_shape()?;
94 value.signature = sign(owner_device_signer, OFFER_DOMAIN, &value.signed_fields());
95 Ok(value)
96 }
97
98 pub fn verify(&self, owner: &StoreDeviceRegistration) -> Result<(), DeviceJoinError> {
99 self.validate_shape()?;
100 self.owner_registration.verify_registration(owner)?;
101 verify_signature(
102 &owner.device_signing_pubkey,
103 &self.signature,
104 OFFER_DOMAIN,
105 &self.signed_fields(),
106 )
107 }
108
109 pub fn offer_hash(&self) -> ObjectHash {
110 ObjectHash::digest(&domain_json(OFFER_DOMAIN, &self.signed_fields()))
111 }
112
113 fn validate_shape(&self) -> Result<(), DeviceJoinError> {
114 if self.version != STORE_PROTOCOL_VERSION
115 || self.member_pubkey.is_empty()
116 || self.provider_admin.administrator != self.owner_registration
117 && self.provider_admin.administrator.device_id == self.owner_registration.device_id
118 || self.attempt_slot == self.outcome_slot
119 {
120 return Err(DeviceJoinError::OfferMismatch);
121 }
122 self.provider.validate()?;
123 self.provider_admin
124 .provider
125 .validate_for(&self.provider)
126 .map_err(DeviceJoinError::Storage)?;
127 if let crate::sync::provider::ProviderAdminGrantOrigin::Founder { root } =
128 &self.provider_admin.created_at
129 {
130 if root != &self.store_root {
131 return Err(DeviceJoinError::OfferMismatch);
132 }
133 }
134 Ok(())
135 }
136
137 fn signed_fields(&self) -> DeviceJoinOfferFields<'_> {
138 DeviceJoinOfferFields {
139 version: self.version,
140 attempt_id: self.attempt_id,
141 member_pubkey: &self.member_pubkey,
142 store_root: &self.store_root,
143 provider: &self.provider,
144 attempt_slot: &self.attempt_slot,
145 outcome_slot: &self.outcome_slot,
146 owner_registration: &self.owner_registration,
147 owner_grant: &self.owner_grant,
148 provider_admin: &self.provider_admin,
149 }
150 }
151}
152
153#[derive(Serialize)]
154struct DeviceJoinOfferFields<'a> {
155 version: u32,
156 attempt_id: DeviceJoinAttemptId,
157 member_pubkey: &'a str,
158 store_root: &'a StoreRootRef,
159 provider: &'a StoreProviderBinding,
160 attempt_slot: &'a ObjectSlot,
161 outcome_slot: &'a ObjectSlot,
162 owner_registration: &'a StoreDeviceRegistrationRef,
163 owner_grant: &'a MembershipGrantId,
164 provider_admin: &'a ProviderAdminGrantRecord,
165}
166
167#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
168#[serde(deny_unknown_fields)]
169pub struct DeviceProviderAccessRequest {
170 pub offer: Box<DeviceJoinOffer>,
171 pub peer_provider: ProviderDeviceBinding,
172 pub signature: String,
173}
174
175impl DeviceProviderAccessRequest {
176 pub fn signed(
177 offer: DeviceJoinOffer,
178 peer_provider: ProviderDeviceBinding,
179 member_signer: &UserKeypair,
180 ) -> Result<Self, DeviceJoinError> {
181 if keys::public_key_hex(member_signer) != offer.member_pubkey {
182 return Err(DeviceJoinError::InvalidSignature);
183 }
184 peer_provider.validate_for(&offer.provider)?;
185 let mut request = Self {
186 offer: Box::new(offer),
187 peer_provider,
188 signature: String::new(),
189 };
190 request.signature = sign(
191 member_signer,
192 ACCESS_REQUEST_DOMAIN,
193 &request.signed_fields(),
194 );
195 Ok(request)
196 }
197
198 pub fn verify(&self, owner: &StoreDeviceRegistration) -> Result<(), DeviceJoinError> {
199 self.offer.verify(owner)?;
200 self.peer_provider.validate_for(&self.offer.provider)?;
201 verify_signature(
202 &self.offer.member_pubkey,
203 &self.signature,
204 ACCESS_REQUEST_DOMAIN,
205 &self.signed_fields(),
206 )
207 }
208
209 pub fn request_hash(&self) -> ObjectHash {
210 ObjectHash::digest(&domain_json(ACCESS_REQUEST_DOMAIN, &self.signed_fields()))
211 }
212
213 fn signed_fields(&self) -> (&DeviceJoinOffer, &ProviderDeviceBinding) {
214 (&self.offer, &self.peer_provider)
215 }
216}
217
218#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
219#[serde(rename_all = "snake_case", deny_unknown_fields)]
220pub enum DeviceProviderAdmissionChallenge {
221 SamePrincipal,
222 CrossPrincipal(CrossPrincipalProbeChallenge),
223}
224
225#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
226#[serde(deny_unknown_fields)]
227pub struct DeviceProviderAdmissionApproval {
228 pub request: Box<DeviceProviderAccessRequest>,
229 pub access_grant: ActivatedStoreMemberProviderAccessGrant,
230 pub admission: DeviceProviderAdmissionChallenge,
231 pub signature: String,
232}
233
234impl DeviceProviderAdmissionApproval {
235 pub fn signed(
236 request: DeviceProviderAccessRequest,
237 access_grant: ActivatedStoreMemberProviderAccessGrant,
238 admission: DeviceProviderAdmissionChallenge,
239 administrator: &StoreDeviceRegistration,
240 administrator_device_signer: &UserKeypair,
241 ) -> Result<Self, DeviceJoinError> {
242 if keys::public_key_hex(administrator_device_signer) != administrator.device_signing_pubkey
243 {
244 return Err(DeviceJoinError::InvalidSignature);
245 }
246 let mut value = Self {
247 request: Box::new(request),
248 access_grant,
249 admission,
250 signature: String::new(),
251 };
252 value.validate_shape(administrator)?;
253 value.signature = sign(
254 administrator_device_signer,
255 APPROVAL_DOMAIN,
256 &value.signed_fields(),
257 );
258 Ok(value)
259 }
260
261 pub fn verify(
262 &self,
263 owner: &StoreDeviceRegistration,
264 administrator: &StoreDeviceRegistration,
265 ) -> Result<(), DeviceJoinError> {
266 self.request.verify(owner)?;
267 self.validate_shape(administrator)?;
268 verify_signature(
269 &administrator.device_signing_pubkey,
270 &self.signature,
271 APPROVAL_DOMAIN,
272 &self.signed_fields(),
273 )
274 }
275
276 fn validate_shape(
277 &self,
278 administrator: &StoreDeviceRegistration,
279 ) -> Result<(), DeviceJoinError> {
280 let offer = &self.request.offer;
281 if self.access_grant.grant.member_pubkey != offer.member_pubkey
282 || self.access_grant.grant.provider != self.request.peer_provider
283 || self.access_grant.grant_ref.grant_id != self.access_grant.grant.grant_id
284 || self.access_grant.grant_ref.grant_hash != self.access_grant.grant.grant_hash()
285 || self.access_grant.grant.administrator_grant != offer.provider_admin.grant_id
286 || self.access_grant.grant.administrator != offer.provider_admin.administrator
287 || !self
288 .access_grant
289 .activation
290 .coord
291 .policy()
292 .eq(&offer.store_root_policy_from_admin()?)
293 {
294 return Err(DeviceJoinError::ApprovalMismatch);
295 }
296 self.access_grant
297 .grant
298 .verify(&offer.provider, administrator)
299 .map_err(|error| DeviceJoinError::Provider(error.to_string()))?;
300 let same_principal = offer.provider_admin.provider == self.request.peer_provider;
301 if same_principal
302 != matches!(
303 self.admission,
304 DeviceProviderAdmissionChallenge::SamePrincipal
305 )
306 {
307 return Err(DeviceJoinError::ApprovalMismatch);
308 }
309 Ok(())
310 }
311
312 fn signed_fields(
313 &self,
314 ) -> (
315 &DeviceProviderAccessRequest,
316 &ActivatedStoreMemberProviderAccessGrant,
317 &DeviceProviderAdmissionChallenge,
318 ) {
319 (&self.request, &self.access_grant, &self.admission)
320 }
321}
322
323trait OfferPolicy {
324 fn store_root_policy_from_admin(&self) -> Result<crate::WritePolicy, DeviceJoinError>;
325}
326
327impl OfferPolicy for DeviceJoinOffer {
328 fn store_root_policy_from_admin(&self) -> Result<crate::WritePolicy, DeviceJoinError> {
329 match &self.provider_admin.created_at {
330 crate::sync::provider::ProviderAdminGrantOrigin::Founder { .. } => Ok(self
331 .provider_admin
332 .capability
333 .serial_coordination
334 .as_ref()
335 .map_or(crate::WritePolicy::MergeConcurrent, |_| {
336 crate::WritePolicy::Serial
337 })),
338 crate::sync::provider::ProviderAdminGrantOrigin::MergeMembership { .. } => {
339 Ok(crate::WritePolicy::MergeConcurrent)
340 }
341 crate::sync::provider::ProviderAdminGrantOrigin::SerialCommit { .. } => {
342 Ok(crate::WritePolicy::Serial)
343 }
344 }
345 }
346}
347
348#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
349#[serde(rename_all = "snake_case", deny_unknown_fields)]
350pub enum DeviceProviderResponseReservation {
351 SamePrincipal,
352 CrossPrincipal { response_slot: ObjectSlot },
353}
354
355#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
356#[serde(deny_unknown_fields)]
357pub struct DeviceRegistrationRequest {
358 pub approval: Box<DeviceProviderAdmissionApproval>,
359 pub expected_registration: StoreDeviceRegistration,
360 pub registration_slot: ObjectSlot,
361 pub response: DeviceProviderResponseReservation,
362 pub signature: String,
363}
364
365impl DeviceRegistrationRequest {
366 pub fn signed(
367 approval: DeviceProviderAdmissionApproval,
368 expected_registration: StoreDeviceRegistration,
369 registration_slot: ObjectSlot,
370 response: DeviceProviderResponseReservation,
371 member_signer: &UserKeypair,
372 ) -> Result<Self, DeviceJoinError> {
373 if keys::public_key_hex(member_signer) != approval.request.offer.member_pubkey {
374 return Err(DeviceJoinError::InvalidSignature);
375 }
376 let mut value = Self {
377 approval: Box::new(approval),
378 expected_registration,
379 registration_slot,
380 response,
381 signature: String::new(),
382 };
383 value.validate_shape()?;
384 value.signature = sign(
385 member_signer,
386 REGISTRATION_REQUEST_DOMAIN,
387 &value.signed_fields(),
388 );
389 Ok(value)
390 }
391
392 pub fn verify(&self) -> Result<(), DeviceJoinError> {
393 self.validate_shape()?;
394 verify_signature(
395 &self.approval.request.offer.member_pubkey,
396 &self.signature,
397 REGISTRATION_REQUEST_DOMAIN,
398 &self.signed_fields(),
399 )
400 }
401
402 fn validate_shape(&self) -> Result<(), DeviceJoinError> {
403 let offer = &self.approval.request.offer;
404 if self.expected_registration.store_root != offer.store_root
405 || self.expected_registration.author_pubkey != offer.member_pubkey
406 || self.expected_registration.provider != self.approval.request.peer_provider
407 || self.registration_slot == offer.attempt_slot
408 || self.registration_slot == offer.outcome_slot
409 {
410 return Err(DeviceJoinError::RegistrationRequestMismatch);
411 }
412 match (
413 &self.approval.admission,
414 &self.response,
415 &self.expected_registration.origin,
416 ) {
417 (
418 DeviceProviderAdmissionChallenge::SamePrincipal,
419 DeviceProviderResponseReservation::SamePrincipal,
420 crate::sync::store_commit::StoreDeviceRegistrationOrigin::Join {
421 attempt_id,
422 attempt_slot,
423 outcome_slot,
424 },
425 )
426 | (
427 DeviceProviderAdmissionChallenge::CrossPrincipal(_),
428 DeviceProviderResponseReservation::CrossPrincipal { .. },
429 crate::sync::store_commit::StoreDeviceRegistrationOrigin::Join {
430 attempt_id,
431 attempt_slot,
432 outcome_slot,
433 },
434 ) if *attempt_id == offer.attempt_id
435 && attempt_slot == &offer.attempt_slot
436 && outcome_slot == &offer.outcome_slot => {}
437 _ => return Err(DeviceJoinError::RegistrationRequestMismatch),
438 }
439 let mut slots = vec![
440 offer.attempt_slot.clone(),
441 offer.outcome_slot.clone(),
442 self.registration_slot.clone(),
443 self.expected_registration
444 .acknowledgements
445 .first_slot()
446 .clone(),
447 ];
448 if let DeviceProviderAdmissionChallenge::CrossPrincipal(challenge) =
449 &self.approval.admission
450 {
451 slots.push(challenge.administrator_object.slot.clone());
452 }
453 if let DeviceProviderResponseReservation::CrossPrincipal { response_slot } = &self.response
454 {
455 slots.push(response_slot.clone());
456 }
457 require_distinct_slots(&slots)?;
458 Ok(())
459 }
460
461 fn signed_fields(
462 &self,
463 ) -> (
464 &DeviceProviderAdmissionApproval,
465 &StoreDeviceRegistration,
466 &ObjectSlot,
467 &DeviceProviderResponseReservation,
468 ) {
469 (
470 &self.approval,
471 &self.expected_registration,
472 &self.registration_slot,
473 &self.response,
474 )
475 }
476}
477
478trait DeviceStreamFirstSlot {
479 fn first_slot(&self) -> &ObjectSlot;
480}
481
482impl DeviceStreamFirstSlot for crate::sync::store_commit::DeviceStreamAnchor {
483 fn first_slot(&self) -> &ObjectSlot {
484 match self {
485 Self::StoreAnnouncements { first_slot }
486 | Self::StoreAcknowledgements { first_slot }
487 | Self::StoreSnapshots { first_slot }
488 | Self::CircleAcknowledgements { first_slot, .. }
489 | Self::CircleSnapshots { first_slot, .. } => first_slot,
490 }
491 }
492}
493
494#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
495#[serde(deny_unknown_fields)]
496pub struct ProvisionalDeviceBootstrap {
497 pub request: Box<DeviceRegistrationRequest>,
498 pub publication_authorization: DeviceJoinChallengePublicationAuthorization,
499}
500
501#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
502#[serde(rename_all = "snake_case", deny_unknown_fields)]
503pub enum DeviceProviderChallengePublication {
504 SamePrincipal,
505 CrossPrincipal {
506 challenge: CrossPrincipalProbeChallenge,
507 },
508}
509
510#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
511#[serde(deny_unknown_fields)]
512pub struct ProviderReadyDeviceBootstrap {
513 pub bootstrap: Box<ProvisionalDeviceBootstrap>,
514 pub challenge_publication: DeviceProviderChallengePublication,
515}
516
517#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
518#[serde(rename_all = "snake_case", deny_unknown_fields)]
519pub enum DeviceProviderReadiness {
520 SamePrincipal,
521 CrossPrincipal(CrossPrincipalProbeResponse),
522}
523
524#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
525#[serde(deny_unknown_fields)]
526pub struct DeviceJoinReadiness {
527 pub proof: DeviceReadinessProof,
528 pub provider: DeviceProviderReadiness,
529}
530
531#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
532#[serde(rename_all = "snake_case", deny_unknown_fields)]
533pub enum DeviceProviderAdmission {
534 SamePrincipal,
535 CrossPrincipal(CrossPrincipalProbeReceipt),
536}
537
538#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
539#[serde(deny_unknown_fields)]
540pub struct DeviceProviderAdmissionCompletion {
541 pub readiness: Box<DeviceJoinReadiness>,
542 pub admission: DeviceProviderAdmission,
543}
544
545#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
546#[serde(deny_unknown_fields)]
547pub struct DeviceJoinActivation {
548 pub outcome: DeviceJoinOutcomeRef,
549 pub outcome_activation: StoreBatchCommitRef,
550}
551
552#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
553#[serde(deny_unknown_fields)]
554pub struct DeviceJoinCancellation {
555 pub outcome: DeviceJoinOutcomeRef,
556 pub outcome_activation: StoreBatchCommitRef,
557}
558
559#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
560#[serde(deny_unknown_fields)]
561pub struct DeviceJoinAbandonmentRef {
562 pub attempt_id: DeviceJoinAttemptId,
563 pub abandonment_hash: ObjectHash,
564 pub object: ExactObjectRef,
565}
566
567impl DeviceJoinAbandonmentRef {
568 pub(crate) fn verify(
569 &self,
570 abandonment: &DeviceJoinAbandonmentObject,
571 owner: &StoreDeviceRegistration,
572 ) -> Result<(), DeviceJoinError> {
573 abandonment.owner_registration.verify_registration(owner)?;
574 if self.attempt_id != abandonment.attempt_id
575 || self.abandonment_hash != abandonment.abandonment_hash()
576 {
577 return Err(DeviceJoinError::AttemptMismatch);
578 }
579 verify_signature(
580 &owner.device_signing_pubkey,
581 &abandonment.signature,
582 ABANDONMENT_DOMAIN,
583 &abandonment.signed_fields(),
584 )
585 }
586}
587
588#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
589#[serde(deny_unknown_fields)]
590pub struct DeviceJoinAbandonmentObject {
591 pub version: u32,
592 pub store_root_hash: ObjectHash,
593 pub offer_hash: ObjectHash,
594 pub attempt_id: DeviceJoinAttemptId,
595 pub attempt_slot: ObjectSlot,
596 pub owner_registration: StoreDeviceRegistrationRef,
597 pub owner_grant: MembershipGrantId,
598 pub signature: String,
599}
600
601impl DeviceJoinAbandonmentObject {
602 pub fn signed(
603 offer: &DeviceJoinOffer,
604 owner: &StoreDeviceRegistration,
605 owner_device_signer: &UserKeypair,
606 ) -> Result<Self, DeviceJoinError> {
607 offer.verify(owner)?;
608 if keys::public_key_hex(owner_device_signer) != owner.device_signing_pubkey {
609 return Err(DeviceJoinError::InvalidSignature);
610 }
611 let mut value = Self {
612 version: STORE_PROTOCOL_VERSION,
613 store_root_hash: offer.store_root.store_root_hash,
614 offer_hash: offer.offer_hash(),
615 attempt_id: offer.attempt_id,
616 attempt_slot: offer.attempt_slot.clone(),
617 owner_registration: offer.owner_registration.clone(),
618 owner_grant: offer.owner_grant.clone(),
619 signature: String::new(),
620 };
621 value.signature = sign(
622 owner_device_signer,
623 ABANDONMENT_DOMAIN,
624 &value.signed_fields(),
625 );
626 Ok(value)
627 }
628
629 pub fn abandonment_hash(&self) -> ObjectHash {
630 ObjectHash::digest(&domain_json(ABANDONMENT_DOMAIN, &self.signed_fields()))
631 }
632
633 pub fn to_bytes(&self) -> Vec<u8> {
634 serde_json::to_vec(self).expect("device join abandonment serialization cannot fail")
635 }
636
637 fn signed_fields(
638 &self,
639 ) -> (
640 u32,
641 ObjectHash,
642 ObjectHash,
643 DeviceJoinAttemptId,
644 &ObjectSlot,
645 &StoreDeviceRegistrationRef,
646 &MembershipGrantId,
647 ) {
648 (
649 self.version,
650 self.store_root_hash,
651 self.offer_hash,
652 self.attempt_id,
653 &self.attempt_slot,
654 &self.owner_registration,
655 &self.owner_grant,
656 )
657 }
658}
659
660#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
661#[serde(deny_unknown_fields)]
662pub struct DeviceJoinAbandonment {
663 pub abandonment: DeviceJoinAbandonmentRef,
664 pub abandonment_activation: StoreBatchCommitRef,
665}
666
667#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
668#[serde(rename_all = "snake_case", deny_unknown_fields)]
669pub enum ProviderChallengeDisposition {
670 SamePrincipal,
671 NeverCreated,
672 Created(ExactObjectRef),
673 AlreadyDeleted(ExactObjectRef),
674}
675
676#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
677#[serde(deny_unknown_fields)]
678pub struct ProviderAdminJoinClosure {
679 pub cancellation: DeviceJoinOutcomeRef,
680 pub administrator_registration: StoreDeviceRegistrationRef,
681 pub challenge: ProviderChallengeDisposition,
682 pub prior_state_hash: ObjectHash,
683 pub signature: String,
684}
685
686impl ProviderAdminJoinClosure {
687 pub fn signed(
688 cancellation: DeviceJoinOutcomeRef,
689 administrator_registration: StoreDeviceRegistrationRef,
690 challenge: ProviderChallengeDisposition,
691 prior_state_hash: ObjectHash,
692 administrator: &StoreDeviceRegistration,
693 signer: &UserKeypair,
694 ) -> Result<Self, DeviceJoinError> {
695 require_cancelled_outcome(&cancellation)?;
696 administrator_registration.verify_registration(administrator)?;
697 if keys::public_key_hex(signer) != administrator.device_signing_pubkey {
698 return Err(DeviceJoinError::InvalidSignature);
699 }
700 let mut value = Self {
701 cancellation,
702 administrator_registration,
703 challenge,
704 prior_state_hash,
705 signature: String::new(),
706 };
707 value.signature = sign(signer, PROVIDER_CLOSURE_DOMAIN, &value.signed_fields());
708 Ok(value)
709 }
710
711 pub fn verify(&self, administrator: &StoreDeviceRegistration) -> Result<(), DeviceJoinError> {
712 require_cancelled_outcome(&self.cancellation)?;
713 self.administrator_registration
714 .verify_registration(administrator)?;
715 verify_signature(
716 &administrator.device_signing_pubkey,
717 &self.signature,
718 PROVIDER_CLOSURE_DOMAIN,
719 &self.signed_fields(),
720 )
721 }
722
723 fn signed_fields(
724 &self,
725 ) -> (
726 &DeviceJoinOutcomeRef,
727 &StoreDeviceRegistrationRef,
728 &ProviderChallengeDisposition,
729 ObjectHash,
730 ) {
731 (
732 &self.cancellation,
733 &self.administrator_registration,
734 &self.challenge,
735 self.prior_state_hash,
736 )
737 }
738}
739
740#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
741#[serde(rename_all = "snake_case", deny_unknown_fields)]
742pub enum SlotDisposition {
743 NeverCreated,
744 Created(ExactObjectRef),
745}
746
747#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
748#[serde(rename_all = "snake_case", deny_unknown_fields)]
749pub enum JoinerResponseDisposition {
750 SamePrincipal,
751 Slot(SlotDisposition),
752}
753
754#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
755#[serde(deny_unknown_fields)]
756pub struct JoinerJoinClosure {
757 pub cancellation: DeviceJoinOutcomeRef,
758 pub expected_registration: StoreDeviceRegistration,
759 pub registration: SlotDisposition,
760 pub initial_ack: SlotDisposition,
761 pub response: JoinerResponseDisposition,
762 pub prior_state_hash: ObjectHash,
763 pub signature: String,
764}
765
766impl JoinerJoinClosure {
767 #[allow(clippy::too_many_arguments)]
768 pub fn signed(
769 cancellation: DeviceJoinOutcomeRef,
770 expected_registration: StoreDeviceRegistration,
771 registration: SlotDisposition,
772 initial_ack: SlotDisposition,
773 response: JoinerResponseDisposition,
774 prior_state_hash: ObjectHash,
775 signer: &UserKeypair,
776 ) -> Result<Self, DeviceJoinError> {
777 require_cancelled_outcome(&cancellation)?;
778 if keys::public_key_hex(signer) != expected_registration.device_signing_pubkey {
779 return Err(DeviceJoinError::InvalidSignature);
780 }
781 let mut value = Self {
782 cancellation,
783 expected_registration,
784 registration,
785 initial_ack,
786 response,
787 prior_state_hash,
788 signature: String::new(),
789 };
790 value.signature = sign(signer, JOINER_CLOSURE_DOMAIN, &value.signed_fields());
791 Ok(value)
792 }
793
794 pub fn verify(&self) -> Result<(), DeviceJoinError> {
795 require_cancelled_outcome(&self.cancellation)?;
796 verify_signature(
797 &self.expected_registration.device_signing_pubkey,
798 &self.signature,
799 JOINER_CLOSURE_DOMAIN,
800 &self.signed_fields(),
801 )
802 }
803
804 fn signed_fields(
805 &self,
806 ) -> (
807 &DeviceJoinOutcomeRef,
808 &StoreDeviceRegistration,
809 &SlotDisposition,
810 &SlotDisposition,
811 &JoinerResponseDisposition,
812 ObjectHash,
813 ) {
814 (
815 &self.cancellation,
816 &self.expected_registration,
817 &self.registration,
818 &self.initial_ack,
819 &self.response,
820 self.prior_state_hash,
821 )
822 }
823}
824
825#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
826#[serde(rename_all = "snake_case")]
827pub enum DeviceJoinProducer {
828 ProviderAdministrator,
829 Joiner,
830}
831
832#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
833#[serde(rename_all = "snake_case", deny_unknown_fields)]
834pub enum ProviderWriteAuthorityRef {
835 ProviderAdministrator(ProviderAdminGrantId),
836 MemberAccess(StoreMemberProviderAccessGrantRef),
837}
838
839#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
840#[serde(deny_unknown_fields)]
841pub struct DeviceJoinProducerWriteRevocation {
842 pub cancellation: DeviceJoinOutcomeRef,
843 pub producer: DeviceJoinProducer,
844 pub authority: ProviderWriteAuthorityRef,
845 pub protected_slots: Vec<ObjectSlot>,
846 pub withdrawal: ProviderAccessWithdrawal,
847 pub executor_grant: ProviderAdminGrantId,
848 pub executor: StoreDeviceRegistrationRef,
849 pub signature: String,
850}
851
852impl DeviceJoinProducerWriteRevocation {
853 pub fn signed(
854 cancellation: DeviceJoinOutcomeRef,
855 producer: DeviceJoinProducer,
856 authority: ProviderWriteAuthorityRef,
857 mut protected_slots: Vec<ObjectSlot>,
858 withdrawal: ProviderAccessWithdrawal,
859 executor_grant: ProviderAdminGrantId,
860 executor: StoreDeviceRegistrationRef,
861 executor_registration: &StoreDeviceRegistration,
862 executor_signer: &UserKeypair,
863 ) -> Result<Self, DeviceJoinError> {
864 require_cancelled_outcome(&cancellation)?;
865 executor.verify_registration(executor_registration)?;
866 if keys::public_key_hex(executor_signer) != executor_registration.device_signing_pubkey {
867 return Err(DeviceJoinError::InvalidSignature);
868 }
869 protected_slots.sort();
870 if protected_slots.is_empty() || protected_slots.windows(2).any(|pair| pair[0] == pair[1]) {
871 return Err(DeviceJoinError::CleanupMismatch);
872 }
873 let mut value = Self {
874 cancellation,
875 producer,
876 authority,
877 protected_slots,
878 withdrawal,
879 executor_grant,
880 executor,
881 signature: String::new(),
882 };
883 value.signature = sign(
884 executor_signer,
885 WRITE_REVOCATION_DOMAIN,
886 &value.signed_fields(),
887 );
888 Ok(value)
889 }
890
891 pub fn verify(&self, executor: &StoreDeviceRegistration) -> Result<(), DeviceJoinError> {
892 require_cancelled_outcome(&self.cancellation)?;
893 self.executor.verify_registration(executor)?;
894 verify_signature(
895 &executor.device_signing_pubkey,
896 &self.signature,
897 WRITE_REVOCATION_DOMAIN,
898 &self.signed_fields(),
899 )
900 }
901
902 fn signed_fields(
903 &self,
904 ) -> (
905 &DeviceJoinOutcomeRef,
906 DeviceJoinProducer,
907 &ProviderWriteAuthorityRef,
908 &[ObjectSlot],
909 &ProviderAccessWithdrawal,
910 &ProviderAdminGrantId,
911 &StoreDeviceRegistrationRef,
912 ) {
913 (
914 &self.cancellation,
915 self.producer,
916 &self.authority,
917 &self.protected_slots,
918 &self.withdrawal,
919 &self.executor_grant,
920 &self.executor,
921 )
922 }
923}
924
925#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
926#[serde(rename_all = "snake_case", deny_unknown_fields)]
927pub enum ProviderAdminJoinTerminal {
928 Completed(DeviceProviderAdmissionCompletion),
929 Cancelled(ProviderAdminJoinClosure),
930 WriteRevoked(DeviceJoinProducerWriteRevocation),
931}
932
933#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
934#[serde(rename_all = "snake_case", deny_unknown_fields)]
935pub enum JoinerJoinTerminal {
936 Ready(DeviceJoinReadiness),
937 Cancelled(JoinerJoinClosure),
938 WriteRevoked(DeviceJoinProducerWriteRevocation),
939}
940
941#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
942#[serde(deny_unknown_fields)]
943pub struct DeviceJoinCleanupReceiptRef {
944 pub attempt_id: DeviceJoinAttemptId,
945 pub receipt_hash: ObjectHash,
946 pub object: ExactObjectRef,
947}
948
949impl DeviceJoinCleanupReceiptRef {
950 pub(crate) fn verify(
951 &self,
952 receipt: &DeviceJoinCleanupReceiptObject,
953 executor: &StoreDeviceRegistration,
954 ) -> Result<(), DeviceJoinError> {
955 receipt.executor.verify_registration(executor)?;
956 if self.attempt_id != receipt.cancellation.attempt().attempt_id
957 || self.receipt_hash != receipt.receipt_hash()
958 {
959 return Err(DeviceJoinError::CleanupMismatch);
960 }
961 verify_signature(
962 &executor.device_signing_pubkey,
963 &receipt.signature,
964 CLEANUP_RECEIPT_DOMAIN,
965 &receipt.signed_fields(),
966 )
967 }
968}
969
970#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
971#[serde(deny_unknown_fields)]
972pub struct DeviceJoinCleanupReceiptObject {
973 pub version: u32,
974 pub store_root_hash: ObjectHash,
975 pub cancellation: DeviceJoinOutcomeRef,
976 pub administrator_terminal: ProviderAdminJoinTerminal,
977 pub joiner_terminal: JoinerJoinTerminal,
978 pub deleted_slots: Vec<ObjectSlot>,
979 pub membership: StoreMembershipStateRef,
980 pub provider_admin_grant: ProviderAdminGrantId,
981 pub executor: StoreDeviceRegistrationRef,
982 pub signature: String,
983}
984
985impl DeviceJoinCleanupReceiptObject {
986 #[allow(clippy::too_many_arguments)]
987 pub fn signed(
988 attempt: &DeviceJoinAttempt,
989 cancellation: DeviceJoinOutcomeRef,
990 administrator_terminal: ProviderAdminJoinTerminal,
991 joiner_terminal: JoinerJoinTerminal,
992 deleted_slots: Vec<ObjectSlot>,
993 membership: StoreMembershipStateRef,
994 provider_admin_grant: ProviderAdminGrantId,
995 executor: StoreDeviceRegistrationRef,
996 executor_registration: &StoreDeviceRegistration,
997 executor_signer: &UserKeypair,
998 ) -> Result<Self, DeviceJoinError> {
999 require_cancelled_outcome(&cancellation)?;
1000 if cancellation.attempt().attempt_id != attempt.attempt_id {
1001 return Err(DeviceJoinError::AttemptMismatch);
1002 }
1003 executor.verify_registration(executor_registration)?;
1004 if executor_registration.store_root != attempt.store_root
1005 || keys::public_key_hex(executor_signer) != executor_registration.device_signing_pubkey
1006 {
1007 return Err(DeviceJoinError::InvalidSignature);
1008 }
1009 let mut value = Self {
1010 version: STORE_PROTOCOL_VERSION,
1011 store_root_hash: attempt.store_root.store_root_hash,
1012 cancellation,
1013 administrator_terminal,
1014 joiner_terminal,
1015 deleted_slots,
1016 membership,
1017 provider_admin_grant,
1018 executor,
1019 signature: String::new(),
1020 };
1021 value.validate_shape(attempt)?;
1022 value.signature = sign(
1023 executor_signer,
1024 CLEANUP_RECEIPT_DOMAIN,
1025 &value.signed_fields(),
1026 );
1027 Ok(value)
1028 }
1029
1030 pub fn receipt_hash(&self) -> ObjectHash {
1031 ObjectHash::digest(&domain_json(CLEANUP_RECEIPT_DOMAIN, &self.signed_fields()))
1032 }
1033
1034 pub(crate) fn verify(
1035 &self,
1036 attempt: &DeviceJoinAttempt,
1037 executor: &StoreDeviceRegistration,
1038 ) -> Result<(), DeviceJoinError> {
1039 if self.version != STORE_PROTOCOL_VERSION
1040 || self.store_root_hash != attempt.store_root.store_root_hash
1041 {
1042 return Err(DeviceJoinError::CleanupMismatch);
1043 }
1044 let mut verified = self.clone();
1045 verified.validate_shape(attempt)?;
1046 verify_signature(
1047 &executor.device_signing_pubkey,
1048 &self.signature,
1049 CLEANUP_RECEIPT_DOMAIN,
1050 &self.signed_fields(),
1051 )
1052 }
1053
1054 pub fn to_bytes(&self) -> Vec<u8> {
1055 serde_json::to_vec(self).expect("device join cleanup receipt serialization cannot fail")
1056 }
1057
1058 fn validate_shape(&mut self, attempt: &DeviceJoinAttempt) -> Result<(), DeviceJoinError> {
1059 validate_terminals(
1060 &self.cancellation,
1061 &self.administrator_terminal,
1062 &self.joiner_terminal,
1063 )?;
1064 let expected = canonical_cleanup_slots(attempt)?;
1065 self.deleted_slots.sort();
1066 if self.deleted_slots != expected {
1067 return Err(DeviceJoinError::CleanupMismatch);
1068 }
1069 Ok(())
1070 }
1071
1072 fn signed_fields(
1073 &self,
1074 ) -> (
1075 u32,
1076 ObjectHash,
1077 &DeviceJoinOutcomeRef,
1078 &ProviderAdminJoinTerminal,
1079 &JoinerJoinTerminal,
1080 &[ObjectSlot],
1081 &StoreMembershipStateRef,
1082 &ProviderAdminGrantId,
1083 &StoreDeviceRegistrationRef,
1084 ) {
1085 (
1086 self.version,
1087 self.store_root_hash,
1088 &self.cancellation,
1089 &self.administrator_terminal,
1090 &self.joiner_terminal,
1091 &self.deleted_slots,
1092 &self.membership,
1093 &self.provider_admin_grant,
1094 &self.executor,
1095 )
1096 }
1097}
1098
1099#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1100#[serde(deny_unknown_fields)]
1101pub struct DeviceJoinCleanupReceipt {
1102 pub receipt: DeviceJoinCleanupReceiptRef,
1103}
1104
1105#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1106#[serde(deny_unknown_fields)]
1107pub struct DeviceJoinCleanupActivation {
1108 pub receipt: DeviceJoinCleanupReceiptRef,
1109 pub activation: StoreBatchCommitRef,
1110}
1111
1112#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1113#[serde(deny_unknown_fields)]
1114pub struct JoinedStore {
1115 pub store_root: StoreRootRef,
1116 pub registration: StoreDeviceRegistrationRef,
1117 pub activation: DeviceJoinActivation,
1118}
1119
1120#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1121#[serde(rename_all = "snake_case", deny_unknown_fields)]
1122pub enum DeviceJoinCleanupProgress {
1123 AwaitingBoth,
1124 AwaitingAdministrator {
1125 joiner: JoinerJoinTerminal,
1126 },
1127 AwaitingJoiner {
1128 administrator: ProviderAdminJoinTerminal,
1129 },
1130 Ready {
1131 administrator: ProviderAdminJoinTerminal,
1132 joiner: JoinerJoinTerminal,
1133 },
1134}
1135
1136#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1137#[serde(rename_all = "snake_case", deny_unknown_fields)]
1138pub enum DeviceJoinStatus {
1139 OperationInProgress {
1140 attempt_id: DeviceJoinAttemptId,
1141 },
1142 AwaitingAccessRequest {
1143 offer: DeviceJoinOffer,
1144 },
1145 AwaitingProviderAdmission {
1146 request: DeviceProviderAccessRequest,
1147 },
1148 AwaitingRegistrationRequest {
1149 approval: DeviceProviderAdmissionApproval,
1150 },
1151 AwaitingBootstrap {
1152 request: DeviceRegistrationRequest,
1153 },
1154 AwaitingChallengePublication {
1155 bootstrap: ProvisionalDeviceBootstrap,
1156 },
1157 AwaitingReadiness {
1158 bootstrap: ProviderReadyDeviceBootstrap,
1159 },
1160 AwaitingProviderCompletion {
1161 readiness: DeviceJoinReadiness,
1162 },
1163 AwaitingActivation {
1164 completion: DeviceProviderAdmissionCompletion,
1165 },
1166 AwaitingCompletion {
1167 activation: DeviceJoinActivation,
1168 },
1169 Activated {
1170 store: JoinedStore,
1171 },
1172 Abandoned {
1173 abandonment: DeviceJoinAbandonment,
1174 },
1175 ProviderAccessGrantCreatePending {
1176 request: DeviceProviderAccessRequest,
1177 grant: StoreMemberProviderAccessGrant,
1178 },
1179 AbandonmentCreatePending {
1180 abandonment: DeviceJoinAbandonmentRef,
1181 },
1182 CancellationCreatePending {
1183 cancellation: DeviceJoinOutcomeRef,
1184 },
1185 ProviderClosurePending {
1186 cancellation: DeviceJoinCancellation,
1187 producer: DeviceJoinProducer,
1188 },
1189 ProviderClosed {
1190 terminal: ProviderAdminJoinTerminal,
1191 },
1192 JoinerClosed {
1193 terminal: JoinerJoinTerminal,
1194 },
1195 CleanupReceiptCreatePending {
1196 cancellation: DeviceJoinCancellation,
1197 receipt: DeviceJoinCleanupReceiptRef,
1198 },
1199 Cancelled {
1200 cancellation: DeviceJoinCancellation,
1201 },
1202 CleanupPending {
1203 cancellation: DeviceJoinCancellation,
1204 progress: DeviceJoinCleanupProgress,
1205 },
1206 AwaitingCleanupActivation {
1207 receipt: DeviceJoinCleanupReceipt,
1208 },
1209 CleanupActivated {
1210 activation: DeviceJoinCleanupActivation,
1211 },
1212}
1213
1214#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1215#[serde(rename_all = "snake_case", deny_unknown_fields)]
1216pub enum DeviceJoinAction {
1217 TransferOffer(DeviceJoinOffer),
1218 TransferProviderAccessRequest(DeviceProviderAccessRequest),
1219 TransferProviderAdmissionApproval(DeviceProviderAdmissionApproval),
1220 TransferRegistrationRequest(DeviceRegistrationRequest),
1221 TransferProvisionalBootstrap(ProvisionalDeviceBootstrap),
1222 TransferProviderReadyBootstrap(ProviderReadyDeviceBootstrap),
1223 TransferReadiness(DeviceJoinReadiness),
1224 TransferProviderAdmissionCompletion(DeviceProviderAdmissionCompletion),
1225 TransferActivation(DeviceJoinActivation),
1226 TransferAbandonment(DeviceJoinAbandonment),
1227 TransferCancellation(DeviceJoinCancellation),
1228 TransferProviderAdminClosure(ProviderAdminJoinClosure),
1229 TransferJoinerClosure(JoinerJoinClosure),
1230 TransferCleanupReceipt(DeviceJoinCleanupReceipt),
1231 TransferCleanupActivation(DeviceJoinCleanupActivation),
1232 Complete(DeviceJoinActivation),
1233 RetryProviderOperation { attempt_id: DeviceJoinAttemptId },
1234}
1235
1236#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1237#[serde(rename_all = "snake_case", deny_unknown_fields)]
1238pub enum OwnerJoinProgress {
1239 Offered(DeviceJoinOffer),
1240 RegistrationRequested(DeviceRegistrationRequest),
1241 AbandonmentCreateIntent {
1242 offer: DeviceJoinOffer,
1243 abandonment: DeviceJoinAbandonmentRef,
1244 prepared: PreparedDeviceJoinObject,
1245 },
1246 AttemptActivated(ProvisionalDeviceBootstrap),
1247 ProviderReady(ProviderReadyDeviceBootstrap),
1248 AdmissionCompleted(DeviceProviderAdmissionCompletion),
1249 CancellationCreateIntent {
1250 attempt: DeviceJoinAttemptRef,
1251 cancellation: DeviceJoinOutcomeRef,
1252 prepared: PreparedDeviceJoinObject,
1253 },
1254 ActivationPrepared(DeviceJoinActivation),
1255 Activated(JoinedStore),
1256 Abandoned(DeviceJoinAbandonment),
1257 Cancelled(DeviceJoinCancellation),
1258 CleanupReceiptCreateIntent {
1259 cancellation: DeviceJoinCancellation,
1260 receipt: DeviceJoinCleanupReceiptRef,
1261 prepared: PreparedDeviceJoinObject,
1262 },
1263 CleanupReceipt(DeviceJoinCleanupReceipt),
1264 CleanupActivated(DeviceJoinCleanupActivation),
1265 CancelledComplete(DeviceJoinCleanupActivation),
1266}
1267
1268#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1269#[serde(rename_all = "snake_case", deny_unknown_fields)]
1270pub enum ProviderAdminJoinProgress {
1271 AccessRequested(DeviceProviderAccessRequest),
1272 AccessGrantPrepared {
1273 request: DeviceProviderAccessRequest,
1274 grant: StoreMemberProviderAccessGrant,
1275 prepared: PreparedDeviceJoinObject,
1276 },
1277 ApprovalPrepared(DeviceProviderAdmissionApproval),
1278 AttemptObserved(ProvisionalDeviceBootstrap),
1279 ChallengeCreateIntent(ProvisionalDeviceBootstrap),
1280 ProviderReady(ProviderReadyDeviceBootstrap),
1281 ResponseObserved(DeviceJoinReadiness),
1282 CleanupIntent {
1283 cancellation: DeviceJoinCancellation,
1284 challenge: ProviderChallengeDisposition,
1285 prior_state_hash: ObjectHash,
1286 },
1287 Completed(DeviceProviderAdmissionCompletion),
1288 Cancelled(ProviderAdminJoinClosure),
1289 WriteRevoked(DeviceJoinProducerWriteRevocation),
1290}
1291
1292#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1293#[serde(deny_unknown_fields)]
1294pub struct PreparedDeviceJoinObject {
1295 pub object: ExactObjectRef,
1296 pub stored_bytes: Vec<u8>,
1297}
1298
1299impl PreparedDeviceJoinObject {
1300 fn from_prepared(prepared: &crate::sync::storage::PreparedExactObject) -> Self {
1301 Self {
1302 object: prepared.reference().clone(),
1303 stored_bytes: prepared.stored_bytes().to_vec(),
1304 }
1305 }
1306}
1307
1308#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1309#[serde(rename_all = "snake_case", deny_unknown_fields)]
1310pub enum JoinerJoinProgress {
1311 OfferReceived(DeviceJoinOffer),
1312 AccessRequested(DeviceProviderAccessRequest),
1313 ApprovalReceived(DeviceProviderAdmissionApproval),
1314 RegistrationPrepared(DeviceRegistrationRequest),
1315 ProviderReady(ProviderReadyDeviceBootstrap),
1316 RegistrationCreateIntent(ProviderReadyDeviceBootstrap),
1317 RegistrationCreated(StoreDeviceRegistrationRef),
1318 AckCreateIntent(StoreDeviceRegistrationRef),
1319 AckCreated(crate::sync::store_commit::StoreAckRef),
1320 ResponseCreateIntent(DeviceJoinReadiness),
1321 Ready(DeviceJoinReadiness),
1322 ActivationObserved(DeviceJoinActivation),
1323 Activated(JoinedStore),
1324 Abandoned(DeviceJoinAbandonment),
1325 CleanupIntent {
1326 cancellation: DeviceJoinCancellation,
1327 registration: SlotDisposition,
1328 initial_ack: SlotDisposition,
1329 response: JoinerResponseDisposition,
1330 prior_state_hash: ObjectHash,
1331 },
1332 Cancelled(JoinerJoinClosure),
1333 WriteRevoked(DeviceJoinProducerWriteRevocation),
1334 CleanupActivated(DeviceJoinCleanupActivation),
1335 CancelledComplete(DeviceJoinCleanupActivation),
1336}
1337
1338#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1339#[serde(rename_all = "snake_case", deny_unknown_fields)]
1340pub enum DeviceJoinRoleProgress {
1341 Owner(OwnerJoinProgress),
1342 ProviderAdministrator(ProviderAdminJoinProgress),
1343 Joiner(JoinerJoinProgress),
1344}
1345
1346impl DeviceJoinRoleProgress {
1347 fn role_name(&self) -> &'static str {
1348 match self {
1349 Self::Owner(_) => "owner",
1350 Self::ProviderAdministrator(_) => "provider_administrator",
1351 Self::Joiner(_) => "joiner",
1352 }
1353 }
1354
1355 fn validate_transition(&self, next: &Self) -> Result<(), DeviceJoinError> {
1356 let adjacent = match (self, next) {
1357 (Self::Owner(previous), Self::Owner(next)) => owner_adjacent(previous, next),
1358 (Self::ProviderAdministrator(previous), Self::ProviderAdministrator(next)) => {
1359 provider_admin_adjacent(previous, next)
1360 }
1361 (Self::Joiner(previous), Self::Joiner(next)) => joiner_adjacent(previous, next),
1362 _ => false,
1363 };
1364 if adjacent {
1365 Ok(())
1366 } else {
1367 Err(DeviceJoinError::NonAdjacentJournalTransition)
1368 }
1369 }
1370}
1371
1372#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1373#[serde(deny_unknown_fields)]
1374pub struct DeviceJoinJournalRecord {
1375 pub attempt_id: DeviceJoinAttemptId,
1376 pub progress: Box<DeviceJoinRoleProgress>,
1377}
1378
1379#[derive(Clone, Debug)]
1382pub struct DeviceJoinJournalDatabase {
1383 path: PathBuf,
1384}
1385
1386impl DeviceJoinJournalDatabase {
1387 pub fn open(path: impl AsRef<Path>) -> Result<Self, DeviceJoinError> {
1388 let path = path.as_ref().to_path_buf();
1389 let connection = Connection::open(&path)?;
1390 connection.execute_batch(
1391 "PRAGMA foreign_keys = ON;
1392 CREATE TABLE IF NOT EXISTS device_join_journals (
1393 attempt_id TEXT NOT NULL,
1394 role TEXT NOT NULL,
1395 payload TEXT NOT NULL,
1396 PRIMARY KEY (attempt_id, role)
1397 ) STRICT, WITHOUT ROWID;
1398 CREATE TABLE IF NOT EXISTS pending_join_transfers (
1399 attempt_id TEXT PRIMARY KEY,
1400 payload_hash TEXT NOT NULL,
1401 payload TEXT NOT NULL
1402 ) STRICT, WITHOUT ROWID;",
1403 )?;
1404 Ok(Self { path })
1405 }
1406
1407 pub(crate) fn path(&self) -> &Path {
1408 &self.path
1409 }
1410
1411 pub fn begin(
1412 &self,
1413 record: DeviceJoinJournalRecord,
1414 ) -> Result<DeviceJoinJournalRecord, DeviceJoinError> {
1415 validate_initial_progress(&record.progress)?;
1416 let connection = Connection::open(&self.path)?;
1417 let tx = connection.unchecked_transaction()?;
1418 let attempt_id = attempt_key(record.attempt_id);
1419 let role = record.progress.role_name();
1420 let payload = serde_json::to_string(&record)?;
1421 tx.execute(
1422 "INSERT OR IGNORE INTO device_join_journals (attempt_id, role, payload)
1423 VALUES (?1, ?2, ?3)",
1424 (&attempt_id, role, &payload),
1425 )?;
1426 let actual: String = tx.query_row(
1427 "SELECT payload FROM device_join_journals WHERE attempt_id = ?1 AND role = ?2",
1428 (&attempt_id, role),
1429 |row| row.get(0),
1430 )?;
1431 tx.commit()?;
1432 let actual = serde_json::from_str::<DeviceJoinJournalRecord>(&actual)?;
1433 if actual != record {
1434 return Err(DeviceJoinError::JournalConflict);
1435 }
1436 Ok(actual)
1437 }
1438
1439 pub fn load(
1440 &self,
1441 attempt_id: DeviceJoinAttemptId,
1442 role: DeviceJoinRole,
1443 ) -> Result<Option<DeviceJoinJournalRecord>, DeviceJoinError> {
1444 let connection = Connection::open(&self.path)?;
1445 let raw = connection
1446 .query_row(
1447 "SELECT payload FROM device_join_journals WHERE attempt_id = ?1 AND role = ?2",
1448 (attempt_key(attempt_id), role.as_str()),
1449 |row| row.get::<_, String>(0),
1450 )
1451 .optional()?;
1452 raw.map(|value| serde_json::from_str(&value).map_err(DeviceJoinError::from))
1453 .transpose()
1454 }
1455
1456 pub fn advance(
1457 &self,
1458 previous: &DeviceJoinJournalRecord,
1459 next: DeviceJoinJournalRecord,
1460 ) -> Result<(), DeviceJoinError> {
1461 if previous.attempt_id != next.attempt_id {
1462 return Err(DeviceJoinError::JournalConflict);
1463 }
1464 previous.progress.validate_transition(&next.progress)?;
1465 let previous_payload = serde_json::to_string(previous)?;
1466 let next_payload = serde_json::to_string(&next)?;
1467 let connection = Connection::open(&self.path)?;
1468 let changed = connection.execute(
1469 "UPDATE device_join_journals SET payload = ?1
1470 WHERE attempt_id = ?2 AND role = ?3 AND payload = ?4",
1471 (
1472 &next_payload,
1473 attempt_key(previous.attempt_id),
1474 previous.progress.role_name(),
1475 &previous_payload,
1476 ),
1477 )?;
1478 if changed != 1 {
1479 return Err(DeviceJoinError::JournalConflict);
1480 }
1481 Ok(())
1482 }
1483
1484 pub fn stage_pending_transfer(
1485 &self,
1486 record: &DeviceJoinJournalRecord,
1487 ) -> Result<ObjectHash, DeviceJoinError> {
1488 if !matches!(*record.progress, DeviceJoinRoleProgress::Joiner(_)) {
1489 return Err(DeviceJoinError::JournalConflict);
1490 }
1491 let payload = serde_json::to_string(record)?;
1492 let payload_hash = ObjectHash::digest(payload.as_bytes());
1493 let connection = Connection::open(&self.path)?;
1494 connection.execute(
1495 "INSERT OR IGNORE INTO pending_join_transfers (attempt_id, payload_hash, payload)
1496 VALUES (?1, ?2, ?3)",
1497 (
1498 attempt_key(record.attempt_id),
1499 payload_hash.to_string(),
1500 &payload,
1501 ),
1502 )?;
1503 let actual: (String, String) = connection.query_row(
1504 "SELECT payload_hash, payload FROM pending_join_transfers WHERE attempt_id = ?1",
1505 [attempt_key(record.attempt_id)],
1506 |row| Ok((row.get(0)?, row.get(1)?)),
1507 )?;
1508 if actual != (payload_hash.to_string(), payload) {
1509 return Err(DeviceJoinError::JournalConflict);
1510 }
1511 Ok(payload_hash)
1512 }
1513
1514 pub fn transfer_pending_to(
1517 &self,
1518 destination: &DeviceJoinJournalDatabase,
1519 attempt_id: DeviceJoinAttemptId,
1520 ) -> Result<DeviceJoinJournalRecord, DeviceJoinError> {
1521 let destination_connection = Connection::open(&destination.path)?;
1522 destination_connection.execute(
1523 "ATTACH DATABASE ?1 AS pending_join_source",
1524 [self.path.to_string_lossy().as_ref()],
1525 )?;
1526 let tx = destination_connection.unchecked_transaction()?;
1527 let attempt = attempt_key(attempt_id);
1528 let (payload_hash, payload): (String, String) = tx.query_row(
1529 "SELECT payload_hash, payload FROM pending_join_source.pending_join_transfers
1530 WHERE attempt_id = ?1",
1531 [&attempt],
1532 |row| Ok((row.get(0)?, row.get(1)?)),
1533 )?;
1534 let calculated = ObjectHash::digest(payload.as_bytes()).to_string();
1535 if calculated != payload_hash {
1536 return Err(DeviceJoinError::PendingTransferHashMismatch);
1537 }
1538 let record: DeviceJoinJournalRecord = serde_json::from_str(&payload)?;
1539 if record.attempt_id != attempt_id
1540 || !matches!(*record.progress, DeviceJoinRoleProgress::Joiner(_))
1541 {
1542 return Err(DeviceJoinError::JournalConflict);
1543 }
1544 tx.execute(
1545 "INSERT OR IGNORE INTO device_join_journals (attempt_id, role, payload)
1546 VALUES (?1, 'joiner', ?2)",
1547 (&attempt, &payload),
1548 )?;
1549 let installed: String = tx.query_row(
1550 "SELECT payload FROM device_join_journals
1551 WHERE attempt_id = ?1 AND role = 'joiner'",
1552 [&attempt],
1553 |row| row.get(0),
1554 )?;
1555 if installed != payload {
1556 return Err(DeviceJoinError::JournalConflict);
1557 }
1558 tx.execute(
1559 "DELETE FROM pending_join_source.pending_join_transfers WHERE attempt_id = ?1",
1560 [&attempt],
1561 )?;
1562 tx.commit()?;
1563 destination_connection.execute_batch("DETACH DATABASE pending_join_source")?;
1564 Ok(record)
1565 }
1566}
1567
1568#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1569pub enum DeviceJoinRole {
1570 Owner,
1571 ProviderAdministrator,
1572 Joiner,
1573}
1574
1575impl DeviceJoinRole {
1576 fn as_str(self) -> &'static str {
1577 match self {
1578 Self::Owner => "owner",
1579 Self::ProviderAdministrator => "provider_administrator",
1580 Self::Joiner => "joiner",
1581 }
1582 }
1583}
1584
1585#[async_trait::async_trait]
1586pub trait DeviceProviderAccessAdministrator: Send + Sync {
1587 async fn grant_member_access(
1588 &self,
1589 member_pubkey: &str,
1590 provider_account_email: Option<&str>,
1591 peer: &ProviderDeviceBinding,
1592 ) -> Result<crate::sync::provider::ProviderAccessLocator, DeviceJoinError>;
1593}
1594
1595#[derive(Clone, Debug)]
1596pub enum DeviceJoinAuthorization {
1597 MergeConcurrent(MembershipChain),
1598 Serial(crate::sync::membership::SerialAuthorizationState),
1599}
1600
1601impl DeviceJoinAuthorization {
1602 fn policy(&self) -> crate::WritePolicy {
1603 match self {
1604 Self::MergeConcurrent(_) => crate::WritePolicy::MergeConcurrent,
1605 Self::Serial(_) => crate::WritePolicy::Serial,
1606 }
1607 }
1608
1609 fn current_members(&self) -> Vec<(String, crate::sync::membership::MemberRole)> {
1610 match self {
1611 Self::MergeConcurrent(membership) => membership.current_members(),
1612 Self::Serial(authorization) => authorization.membership.current_members(),
1613 }
1614 }
1615
1616 fn active_owner_grant(&self, pubkey: &str) -> Option<MembershipGrantId> {
1617 match self {
1618 Self::MergeConcurrent(membership) => membership.active_owner_grant(pubkey),
1619 Self::Serial(authorization) => authorization.membership.active_owner_grant(pubkey),
1620 }
1621 }
1622
1623 fn is_owner_now(&self, pubkey: &str) -> bool {
1624 match self {
1625 Self::MergeConcurrent(membership) => membership.is_owner_now(pubkey),
1626 Self::Serial(authorization) => authorization.membership.is_owner(pubkey),
1627 }
1628 }
1629
1630 fn current_member_provider_email(&self, pubkey: &str) -> Option<&str> {
1631 match self {
1632 Self::MergeConcurrent(membership) => membership.current_member_provider_email(pubkey),
1633 Self::Serial(authorization) => authorization
1634 .membership
1635 .current_member_provider_email(pubkey),
1636 }
1637 }
1638
1639 fn merge_chain(&self) -> Option<&MembershipChain> {
1640 match self {
1641 Self::MergeConcurrent(membership) => Some(membership),
1642 Self::Serial(_) => None,
1643 }
1644 }
1645
1646 fn resolved_provider_admin(
1647 &self,
1648 grant_id: &ProviderAdminGrantId,
1649 ) -> Result<ProviderAdminGrantRecord, DeviceJoinError> {
1650 let state = match self {
1651 Self::MergeConcurrent(membership) => {
1652 let crate::sync::membership::MembershipStatus::Resolved(resolved) =
1653 membership.status()
1654 else {
1655 return Err(DeviceJoinError::MembershipConflict);
1656 };
1657 resolved.provider_admin.combined_state()
1658 }
1659 Self::Serial(authorization) => &authorization.provider_admin,
1660 };
1661 state
1662 .records()
1663 .get(grant_id)
1664 .filter(|record| state.authorizes(grant_id, &record.administrator))
1665 .cloned()
1666 .ok_or(DeviceJoinError::ProviderAdministratorRequired)
1667 }
1668}
1669
1670pub async fn load_current_device_join_authorization(
1671 db: &Database,
1672 storage: &dyn SyncStorage,
1673 coordination: Option<&dyn CoordinationStorage>,
1674) -> Result<DeviceJoinAuthorization, DeviceJoinError> {
1675 match db.write_policy() {
1676 crate::WritePolicy::MergeConcurrent => {
1677 let membership = crate::sync::pull::load_cycle_membership(storage, db)
1678 .await
1679 .map_err(|error| DeviceJoinError::Store(error.to_string()))?;
1680 let chain = membership
1681 .chain
1682 .ok_or(DeviceJoinError::MembershipConflict)?;
1683 Ok(DeviceJoinAuthorization::MergeConcurrent(chain))
1684 }
1685 crate::WritePolicy::Serial => {
1686 let coordination = coordination.ok_or(DeviceJoinError::MembershipConflict)?;
1687 Ok(DeviceJoinAuthorization::Serial(
1688 crate::sync::store_outbound::current_serial_authorization(
1689 db,
1690 storage,
1691 coordination,
1692 )
1693 .await?,
1694 ))
1695 }
1696 }
1697}
1698
1699async fn require_authorization_policy(
1700 db: &Database,
1701 storage: &dyn SyncStorage,
1702 authorization: &DeviceJoinAuthorization,
1703) -> Result<(), DeviceJoinError> {
1704 let root = db
1705 .local_store_root_ref()
1706 .await
1707 .map_err(database_error)?
1708 .ok_or(DeviceJoinError::ActiveDeviceRequired)?;
1709 let root_value = crate::sync::store_objects::load_store_protocol_root(storage, &root)
1710 .await?
1711 .value;
1712 if root_value.descriptor.write_policy != authorization.policy()
1713 || db.write_policy() != root_value.descriptor.write_policy
1714 {
1715 return Err(DeviceJoinError::OfferMismatch);
1716 }
1717 Ok(())
1718}
1719
1720#[allow(clippy::too_many_arguments)]
1721pub async fn begin_device_join(
1722 db: &Database,
1723 storage: &dyn SyncStorage,
1724 authorization: &DeviceJoinAuthorization,
1725 identity_signer: &UserKeypair,
1726 member_pubkey: &str,
1727 provider_admin_grant: ProviderAdminGrantId,
1728) -> Result<DeviceJoinOffer, DeviceJoinError> {
1729 require_authorization_policy(db, storage, authorization).await?;
1730 validate_member_for_join(member_pubkey, &authorization.current_members())?;
1731 let owner_pubkey = keys::public_key_hex(identity_signer);
1732 let owner_grant = authorization
1733 .active_owner_grant(&owner_pubkey)
1734 .ok_or(DeviceJoinError::OwnerAuthorityRequired)?;
1735 let provider_admin = authorization.resolved_provider_admin(&provider_admin_grant)?;
1736 let device_id = db
1737 .get_protocol_state(crate::database::LOCAL_DEVICE_ID_STATE_KEY)
1738 .await
1739 .map_err(database_error)?
1740 .ok_or(DeviceJoinError::ActiveDeviceRequired)?;
1741 let (root, owner_registration, owner, owner_device_signer) =
1742 crate::sync::store_outbound::load_local_store_authority(db, &device_id, identity_signer)
1743 .await?;
1744 let binding = storage.provider_binding().await?;
1745 let attempt_id =
1746 DeviceJoinAttemptId::from_hash(ObjectHash::digest(db.new_write_id().as_str().as_bytes()));
1747 let attempt_context = crate::sync::storage::ProtocolObjectContext::store(
1748 root.store_root_hash,
1749 crate::sync::storage::ProtocolObjectDomain::DeviceJoinAttempt,
1750 );
1751 let attempt_slot = storage
1752 .allocate_protocol_slot(
1753 &attempt_context,
1754 &crate::sync::store_commit::device_join_attempt_semantic_prefix(attempt_id),
1755 ".json",
1756 )
1757 .await?;
1758 let outcome_context = crate::sync::storage::ProtocolObjectContext::store(
1759 root.store_root_hash,
1760 crate::sync::storage::ProtocolObjectDomain::DeviceJoinOutcome,
1761 );
1762 let outcome_slot = storage
1763 .allocate_protocol_slot(
1764 &outcome_context,
1765 &crate::sync::store_commit::device_join_outcome_semantic_prefix(attempt_id),
1766 ".json",
1767 )
1768 .await?;
1769 let offer = DeviceJoinOffer::signed(
1770 attempt_id,
1771 member_pubkey.to_string(),
1772 root,
1773 binding.store,
1774 attempt_slot,
1775 outcome_slot,
1776 owner_registration,
1777 owner_grant,
1778 provider_admin,
1779 &owner,
1780 &owner_device_signer,
1781 )?;
1782 begin_store_journal(
1783 db,
1784 DeviceJoinJournalRecord {
1785 attempt_id,
1786 progress: Box::new(DeviceJoinRoleProgress::Owner(OwnerJoinProgress::Offered(
1787 offer.clone(),
1788 ))),
1789 },
1790 )
1791 .await?;
1792 Ok(offer)
1793}
1794
1795#[allow(clippy::too_many_arguments)]
1796pub async fn abandon_device_join(
1797 db: &Database,
1798 storage: &dyn SyncStorage,
1799 coordination: Option<&dyn CoordinationStorage>,
1800 authorization: &DeviceJoinAuthorization,
1801 identity_signer: &UserKeypair,
1802 offer: DeviceJoinOffer,
1803) -> Result<DeviceJoinAbandonment, DeviceJoinError> {
1804 require_authorization_policy(db, storage, authorization).await?;
1805 let current = load_store_journal(db, offer.attempt_id, DeviceJoinRole::Owner)
1806 .await?
1807 .ok_or(DeviceJoinError::JournalConflict)?;
1808 if let DeviceJoinRoleProgress::Owner(OwnerJoinProgress::Abandoned(existing)) =
1809 &*current.progress
1810 {
1811 return Ok(existing.clone());
1812 }
1813 let owner = db
1814 .activated_store_device_registration(offer.owner_registration.clone())
1815 .await
1816 .map_err(database_error)?;
1817 offer.verify(&owner)?;
1818 let local_device_id = db
1819 .get_protocol_state(crate::database::LOCAL_DEVICE_ID_STATE_KEY)
1820 .await
1821 .map_err(database_error)?
1822 .ok_or(DeviceJoinError::ActiveDeviceRequired)?;
1823 if owner.device_id.to_string() != local_device_id
1824 || !authorization.is_owner_now(&keys::public_key_hex(identity_signer))
1825 {
1826 return Err(DeviceJoinError::OwnerAuthorityRequired);
1827 }
1828 let owner_signer = owner.device_signer(identity_signer)?;
1829 let abandonment_object = DeviceJoinAbandonmentObject::signed(&offer, &owner, &owner_signer)?;
1830 let context = crate::sync::storage::ProtocolObjectContext::store(
1831 offer.store_root.store_root_hash,
1832 crate::sync::storage::ProtocolObjectDomain::DeviceJoinAbandonment,
1833 );
1834 let prefix =
1835 crate::sync::store_commit::device_join_abandonment_semantic_prefix(offer.attempt_id);
1836 let prepared = storage.prepare_protocol_object(
1837 &context,
1838 offer.attempt_slot.clone(),
1839 &prefix,
1840 abandonment_object.to_bytes(),
1841 )?;
1842 let abandonment_ref = DeviceJoinAbandonmentRef {
1843 attempt_id: offer.attempt_id,
1844 abandonment_hash: abandonment_object.abandonment_hash(),
1845 object: prepared.reference().clone(),
1846 };
1847 let intent = DeviceJoinJournalRecord {
1848 attempt_id: offer.attempt_id,
1849 progress: Box::new(DeviceJoinRoleProgress::Owner(
1850 OwnerJoinProgress::AbandonmentCreateIntent {
1851 offer: offer.clone(),
1852 abandonment: abandonment_ref.clone(),
1853 prepared: PreparedDeviceJoinObject::from_prepared(&prepared),
1854 },
1855 )),
1856 };
1857 match &*current.progress {
1858 DeviceJoinRoleProgress::Owner(OwnerJoinProgress::Offered(durable)) if durable == &offer => {
1859 advance_store_journal(db, ¤t, intent.clone()).await?;
1860 }
1861 DeviceJoinRoleProgress::Owner(OwnerJoinProgress::RegistrationRequested(request))
1862 if request.approval.request.offer.as_ref() == &offer =>
1863 {
1864 advance_store_journal(db, ¤t, intent.clone()).await?;
1865 }
1866 DeviceJoinRoleProgress::Owner(OwnerJoinProgress::AbandonmentCreateIntent {
1867 offer: durable_offer,
1868 abandonment,
1869 prepared: durable_prepared,
1870 }) if durable_offer == &offer
1871 && abandonment == &abandonment_ref
1872 && durable_prepared == &PreparedDeviceJoinObject::from_prepared(&prepared) => {}
1873 _ => return Err(DeviceJoinError::JournalConflict),
1874 }
1875 storage.create_protocol_object(&prepared).await?;
1876 let opened = storage
1877 .read_protocol_object(&context, prepared.reference(), &prefix)
1878 .await?;
1879 if opened != abandonment_object.to_bytes() {
1880 return Err(DeviceJoinError::AttemptMismatch);
1881 }
1882 abandonment_ref.verify(&abandonment_object, &owner)?;
1883 let plan = crate::sync::store_outbound::prepare_device_join_commit(
1884 db,
1885 storage,
1886 coordination,
1887 &local_device_id,
1888 identity_signer,
1889 authorization.merge_chain(),
1890 )
1891 .await?;
1892 let activation = crate::sync::store_outbound::activate_device_join_commit(
1893 db,
1894 storage,
1895 coordination,
1896 plan,
1897 crate::sync::store_outbound::DeviceJoinStoreBatch::Abandonment(abandonment_ref.clone()),
1898 )
1899 .await?;
1900 let abandonment = DeviceJoinAbandonment {
1901 abandonment: abandonment_ref,
1902 abandonment_activation: activation,
1903 };
1904 advance_store_journal(
1905 db,
1906 &intent,
1907 DeviceJoinJournalRecord {
1908 attempt_id: offer.attempt_id,
1909 progress: Box::new(DeviceJoinRoleProgress::Owner(OwnerJoinProgress::Abandoned(
1910 abandonment.clone(),
1911 ))),
1912 },
1913 )
1914 .await?;
1915 Ok(abandonment)
1916}
1917
1918pub async fn prepare_device_provider_access_request(
1919 pending: &DeviceJoinJournalDatabase,
1920 provider_binding: crate::sync::storage::ResolvedProviderBinding,
1921 identity_signer: &UserKeypair,
1922 offer: DeviceJoinOffer,
1923) -> Result<DeviceProviderAccessRequest, DeviceJoinError> {
1924 if let Some(record) = pending.load(offer.attempt_id, DeviceJoinRole::Joiner)? {
1925 return match &*record.progress {
1926 DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::AccessRequested(request))
1927 if *request.offer == offer =>
1928 {
1929 Ok(request.clone())
1930 }
1931 DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::OfferReceived(durable))
1932 if durable == &offer =>
1933 {
1934 prepare_new_access_request(
1935 pending,
1936 provider_binding,
1937 identity_signer,
1938 record.clone(),
1939 durable.clone(),
1940 )
1941 .await
1942 }
1943 _ => Err(DeviceJoinError::JournalConflict),
1944 };
1945 }
1946 let initial = DeviceJoinJournalRecord {
1947 attempt_id: offer.attempt_id,
1948 progress: Box::new(DeviceJoinRoleProgress::Joiner(
1949 JoinerJoinProgress::OfferReceived(offer.clone()),
1950 )),
1951 };
1952 let durable = pending.begin(initial.clone())?;
1953 if durable != initial {
1954 return Err(DeviceJoinError::JournalConflict);
1955 }
1956 prepare_new_access_request(pending, provider_binding, identity_signer, initial, offer).await
1957}
1958
1959async fn prepare_new_access_request(
1960 pending: &DeviceJoinJournalDatabase,
1961 provider_binding: crate::sync::storage::ResolvedProviderBinding,
1962 identity_signer: &UserKeypair,
1963 initial: DeviceJoinJournalRecord,
1964 offer: DeviceJoinOffer,
1965) -> Result<DeviceProviderAccessRequest, DeviceJoinError> {
1966 if provider_binding.store != offer.provider {
1967 return Err(DeviceJoinError::OfferMismatch);
1968 }
1969 let request =
1970 DeviceProviderAccessRequest::signed(offer, provider_binding.device, identity_signer)?;
1971 pending.advance(
1972 &initial,
1973 DeviceJoinJournalRecord {
1974 attempt_id: request.offer.attempt_id,
1975 progress: Box::new(DeviceJoinRoleProgress::Joiner(
1976 JoinerJoinProgress::AccessRequested(request.clone()),
1977 )),
1978 },
1979 )?;
1980 Ok(request)
1981}
1982
1983pub async fn prepare_device_registration_request(
1984 pending: &DeviceJoinJournalDatabase,
1985 storage: &dyn SyncStorage,
1986 peer_exact: Option<&dyn ExactSlotStorage>,
1987 identity_signer: &UserKeypair,
1988 approval: DeviceProviderAdmissionApproval,
1989) -> Result<DeviceRegistrationRequest, DeviceJoinError> {
1990 let attempt_id = approval.request.offer.attempt_id;
1991 if let Some(record) = pending.load(attempt_id, DeviceJoinRole::Joiner)? {
1992 if let DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::RegistrationPrepared(request)) =
1993 *record.progress
1994 {
1995 if *request.approval == approval {
1996 return Ok(request);
1997 }
1998 return Err(DeviceJoinError::JournalConflict);
1999 }
2000 }
2001 let owner = crate::sync::store_objects::load_registration_ref(
2002 storage,
2003 &approval.request.offer.store_root,
2004 &approval.request.offer.owner_registration,
2005 )
2006 .await?
2007 .value;
2008 let administrator = crate::sync::store_objects::load_registration_ref(
2009 storage,
2010 &approval.request.offer.store_root,
2011 &approval.request.offer.provider_admin.administrator,
2012 )
2013 .await?
2014 .value;
2015 approval.verify(&owner, &administrator)?;
2016 let live = storage.provider_binding().await?;
2017 if live.store != approval.request.offer.provider
2018 || live.device != approval.request.peer_provider
2019 {
2020 return Err(DeviceJoinError::ApprovalMismatch);
2021 }
2022 let current = pending
2023 .load(attempt_id, DeviceJoinRole::Joiner)?
2024 .ok_or(DeviceJoinError::JournalConflict)?;
2025 let access_request = match &*current.progress {
2026 DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::AccessRequested(request))
2027 if request == &*approval.request =>
2028 {
2029 request.clone()
2030 }
2031 DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::ApprovalReceived(existing))
2032 if existing == &approval =>
2033 {
2034 *approval.request.clone()
2035 }
2036 _ => return Err(DeviceJoinError::JournalConflict),
2037 };
2038 let approval_record = if matches!(
2039 *current.progress,
2040 DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::ApprovalReceived(_))
2041 ) {
2042 current
2043 } else {
2044 let next = DeviceJoinJournalRecord {
2045 attempt_id,
2046 progress: Box::new(DeviceJoinRoleProgress::Joiner(
2047 JoinerJoinProgress::ApprovalReceived(approval.clone()),
2048 )),
2049 };
2050 pending.advance(¤t, next.clone())?;
2051 next
2052 };
2053 let root_value = crate::sync::store_objects::load_store_protocol_root(
2054 storage,
2055 &approval.request.offer.store_root,
2056 )
2057 .await?
2058 .value;
2059 let origin = crate::sync::store_commit::StoreDeviceRegistrationOrigin::Join {
2060 attempt_id,
2061 attempt_slot: approval.request.offer.attempt_slot.clone(),
2062 outcome_slot: approval.request.offer.outcome_slot.clone(),
2063 };
2064 let device_id = crate::sync::store_commit::StoreDeviceId::derive(
2065 &approval.request.offer.store_root,
2066 &origin,
2067 );
2068 let registration_context = crate::sync::storage::ProtocolObjectContext::store(
2069 approval.request.offer.store_root.store_root_hash,
2070 crate::sync::storage::ProtocolObjectDomain::StoreDeviceRegistration,
2071 );
2072 let registration_slot = storage
2073 .allocate_protocol_slot(
2074 ®istration_context,
2075 &crate::sync::store_commit::registration_semantic_prefix(&device_id.to_string()),
2076 ".json",
2077 )
2078 .await?;
2079 let store_commits = match root_value.descriptor.write_policy {
2080 crate::WritePolicy::MergeConcurrent => {
2081 let context = crate::sync::storage::ProtocolObjectContext::store(
2082 approval.request.offer.store_root.store_root_hash,
2083 crate::sync::storage::ProtocolObjectDomain::StoreHead,
2084 );
2085 let first_slot = storage
2086 .allocate_protocol_slot(
2087 &context,
2088 &crate::sync::store_commit::head_slot_prefix(&device_id.to_string(), 1),
2089 ".json",
2090 )
2091 .await?;
2092 crate::sync::store_commit::StoreCommitAnchor::MergeConcurrent {
2093 announcements: crate::sync::store_commit::DeviceStreamAnchor::StoreAnnouncements {
2094 first_slot,
2095 },
2096 }
2097 }
2098 crate::WritePolicy::Serial => crate::sync::store_commit::StoreCommitAnchor::Serial,
2099 };
2100 let ack_context = crate::sync::storage::ProtocolObjectContext::store(
2101 approval.request.offer.store_root.store_root_hash,
2102 crate::sync::storage::ProtocolObjectDomain::StoreAck,
2103 );
2104 let first_ack = storage
2105 .allocate_protocol_slot(
2106 &ack_context,
2107 &crate::sync::store_commit::ack_slot_prefix(&device_id.to_string(), 1),
2108 ".json",
2109 )
2110 .await?;
2111 let snapshot_context = crate::sync::storage::ProtocolObjectContext::store(
2112 approval.request.offer.store_root.store_root_hash,
2113 crate::sync::storage::ProtocolObjectDomain::StoreSnapshotMeta,
2114 );
2115 let first_snapshot = storage
2116 .allocate_protocol_slot(
2117 &snapshot_context,
2118 &crate::sync::store_commit::snapshot_slot_prefix(&device_id.to_string(), 1),
2119 ".json",
2120 )
2121 .await?;
2122 let (response, response_slot) = match &approval.admission {
2123 DeviceProviderAdmissionChallenge::SamePrincipal => {
2124 (DeviceProviderResponseReservation::SamePrincipal, None)
2125 }
2126 DeviceProviderAdmissionChallenge::CrossPrincipal(challenge) => {
2127 let exact = peer_exact.ok_or(DeviceJoinError::ExactSlotStorageRequired)?;
2128 let logical = crate::sync::provider::cross_peer_logical_key(challenge.probe_id);
2129 let slot = exact
2130 .allocate_slot(&logical)
2131 .await
2132 .map_err(provider_error)?;
2133 if slot.logical_key() != logical {
2134 return Err(DeviceJoinError::RegistrationRequestMismatch);
2135 }
2136 (
2137 DeviceProviderResponseReservation::CrossPrincipal {
2138 response_slot: slot.clone(),
2139 },
2140 Some(slot),
2141 )
2142 }
2143 };
2144 let _ = response_slot;
2145 let (registration, _) = crate::sync::store_registration::prepare_registration_for_origin(
2146 storage,
2147 identity_signer,
2148 root_value.descriptor.write_policy,
2149 approval.request.offer.store_root.clone(),
2150 origin,
2151 registration_slot.clone(),
2152 live.device,
2153 store_commits,
2154 crate::sync::store_commit::DeviceStreamAnchor::StoreAcknowledgements {
2155 first_slot: first_ack,
2156 },
2157 crate::sync::store_commit::DeviceStreamAnchor::StoreSnapshots {
2158 first_slot: first_snapshot,
2159 },
2160 )
2161 .await?;
2162 if access_request != *approval.request {
2163 return Err(DeviceJoinError::JournalConflict);
2164 }
2165 let request = DeviceRegistrationRequest::signed(
2166 approval,
2167 registration,
2168 registration_slot,
2169 response,
2170 identity_signer,
2171 )?;
2172 pending.advance(
2173 &approval_record,
2174 DeviceJoinJournalRecord {
2175 attempt_id,
2176 progress: Box::new(DeviceJoinRoleProgress::Joiner(
2177 JoinerJoinProgress::RegistrationPrepared(request.clone()),
2178 )),
2179 },
2180 )?;
2181 Ok(request)
2182}
2183
2184#[allow(clippy::too_many_arguments)]
2185pub async fn accept_device_registration_request(
2186 db: &Database,
2187 storage: &dyn SyncStorage,
2188 coordination: Option<&dyn CoordinationStorage>,
2189 authorization: &DeviceJoinAuthorization,
2190 identity_signer: &UserKeypair,
2191 request: DeviceRegistrationRequest,
2192) -> Result<ProvisionalDeviceBootstrap, DeviceJoinError> {
2193 require_authorization_policy(db, storage, authorization).await?;
2194 request.verify()?;
2195 let offer = &request.approval.request.offer;
2196 let owner = Box::pin(db.activated_store_device_registration(offer.owner_registration.clone()))
2197 .await
2198 .map_err(database_error)?;
2199 let administrator = Box::pin(
2200 db.activated_store_device_registration(offer.provider_admin.administrator.clone()),
2201 )
2202 .await
2203 .map_err(database_error)?;
2204 request.approval.verify(&owner, &administrator)?;
2205 let local_device_id = db
2206 .get_protocol_state(crate::database::LOCAL_DEVICE_ID_STATE_KEY)
2207 .await
2208 .map_err(database_error)?
2209 .ok_or(DeviceJoinError::ActiveDeviceRequired)?;
2210 if owner.device_id.to_string() != local_device_id
2211 || !authorization.is_owner_now(&keys::public_key_hex(identity_signer))
2212 {
2213 return Err(DeviceJoinError::OwnerAuthorityRequired);
2214 }
2215 let owner_signer = owner.device_signer(identity_signer)?;
2216 let offered = DeviceJoinJournalRecord {
2217 attempt_id: offer.attempt_id,
2218 progress: Box::new(DeviceJoinRoleProgress::Owner(OwnerJoinProgress::Offered(
2219 *offer.clone(),
2220 ))),
2221 };
2222 let durable = begin_store_journal(db, offered.clone()).await?;
2223 if let DeviceJoinRoleProgress::Owner(OwnerJoinProgress::AttemptActivated(bootstrap)) =
2224 *durable.progress
2225 {
2226 if *bootstrap.request == request {
2227 return Ok(bootstrap);
2228 }
2229 return Err(DeviceJoinError::JournalConflict);
2230 }
2231 if durable != offered {
2232 return Err(DeviceJoinError::JournalConflict);
2233 }
2234 let requested = DeviceJoinJournalRecord {
2235 attempt_id: offer.attempt_id,
2236 progress: Box::new(DeviceJoinRoleProgress::Owner(
2237 OwnerJoinProgress::RegistrationRequested(request.clone()),
2238 )),
2239 };
2240 advance_store_journal(db, &offered, requested.clone()).await?;
2241 let plan = crate::sync::store_outbound::prepare_device_join_commit(
2242 db,
2243 storage,
2244 coordination,
2245 &local_device_id,
2246 identity_signer,
2247 authorization.merge_chain(),
2248 )
2249 .await?;
2250 let cut = plan.predecessor_cut()?;
2251 if !history_cut_contains(&cut, &request.approval.access_grant.activation) {
2252 return Err(DeviceJoinError::ApprovalActivationMissing);
2253 }
2254 let attempt = DeviceJoinAttempt::signed(
2255 offer.store_root.clone(),
2256 offer.attempt_id,
2257 offer.attempt_slot.clone(),
2258 request.expected_registration.clone(),
2259 request.registration_slot.clone(),
2260 offer.outcome_slot.clone(),
2261 cut,
2262 plan.membership_state().clone(),
2263 offer.provider_admin.grant_id.clone(),
2264 *request.approval.clone(),
2265 request.response.clone(),
2266 offer.owner_registration.clone(),
2267 offer.owner_grant.clone(),
2268 &owner,
2269 &owner_signer,
2270 )?;
2271 let context = crate::sync::storage::ProtocolObjectContext::store(
2272 offer.store_root.store_root_hash,
2273 crate::sync::storage::ProtocolObjectDomain::DeviceJoinAttempt,
2274 );
2275 let prefix = crate::sync::store_commit::device_join_attempt_semantic_prefix(offer.attempt_id);
2276 let prepared = storage.prepare_protocol_object(
2277 &context,
2278 offer.attempt_slot.clone(),
2279 &prefix,
2280 attempt.to_bytes(),
2281 )?;
2282 storage.create_protocol_object(&prepared).await?;
2283 let opened = storage
2284 .read_protocol_object(&context, prepared.reference(), &prefix)
2285 .await?;
2286 if opened != attempt.to_bytes() {
2287 return Err(DeviceJoinError::AttemptMismatch);
2288 }
2289 let attempt_ref = DeviceJoinAttemptRef {
2290 attempt_id: offer.attempt_id,
2291 attempt_hash: attempt.attempt_hash(),
2292 object: prepared.reference().clone(),
2293 };
2294 let activation = crate::sync::store_outbound::activate_device_join_commit(
2295 db,
2296 storage,
2297 coordination,
2298 plan,
2299 crate::sync::store_outbound::DeviceJoinStoreBatch::Attempt(attempt_ref.clone()),
2300 )
2301 .await?;
2302 let attempt_id = offer.attempt_id;
2303 let bootstrap = ProvisionalDeviceBootstrap {
2304 request: Box::new(request),
2305 publication_authorization: DeviceJoinChallengePublicationAuthorization {
2306 attempt: attempt_ref,
2307 attempt_activation: activation,
2308 },
2309 };
2310 advance_store_journal(
2311 db,
2312 &requested,
2313 DeviceJoinJournalRecord {
2314 attempt_id,
2315 progress: Box::new(DeviceJoinRoleProgress::Owner(
2316 OwnerJoinProgress::AttemptActivated(bootstrap.clone()),
2317 )),
2318 },
2319 )
2320 .await?;
2321 Ok(bootstrap)
2322}
2323
2324fn history_cut_contains(
2325 cut: &crate::sync::store_commit::StoreHistoryCut,
2326 expected: &StoreBatchCommitRef,
2327) -> bool {
2328 match cut {
2329 crate::sync::store_commit::StoreHistoryCut::MergeConcurrent(frontier) => {
2330 frontier.values().any(|reference| reference == expected)
2331 }
2332 crate::sync::store_commit::StoreHistoryCut::Serial(
2333 crate::sync::store_commit::StoreSerialPredecessor::Commit(reference),
2334 ) => reference == expected,
2335 crate::sync::store_commit::StoreHistoryCut::Serial(
2336 crate::sync::store_commit::StoreSerialPredecessor::Genesis { .. },
2337 ) => false,
2338 }
2339}
2340
2341pub async fn publish_device_provider_challenge(
2342 db: &Database,
2343 storage: &dyn SyncStorage,
2344 administrator_exact: Option<&dyn ExactSlotStorage>,
2345 bootstrap: ProvisionalDeviceBootstrap,
2346) -> Result<ProviderReadyDeviceBootstrap, DeviceJoinError> {
2347 let offer = &bootstrap.request.approval.request.offer;
2348 let owner = Box::pin(db.activated_store_device_registration(offer.owner_registration.clone()))
2349 .await
2350 .map_err(database_error)?;
2351 let administrator = Box::pin(
2352 db.activated_store_device_registration(offer.provider_admin.administrator.clone()),
2353 )
2354 .await
2355 .map_err(database_error)?;
2356 bootstrap.request.approval.verify(&owner, &administrator)?;
2357 let challenge_publication = match &bootstrap.request.approval.admission {
2358 DeviceProviderAdmissionChallenge::SamePrincipal => {
2359 DeviceProviderChallengePublication::SamePrincipal
2360 }
2361 DeviceProviderAdmissionChallenge::CrossPrincipal(challenge) => {
2362 let exact = administrator_exact.ok_or(DeviceJoinError::ExactSlotStorageRequired)?;
2363 let context = cross_challenge_context(&bootstrap.request.approval.request);
2364 let (_, activation_author) =
2365 Box::pin(crate::sync::store_pull::load_commit_with_author(
2366 storage,
2367 &offer.store_root,
2368 &bootstrap.publication_authorization.attempt_activation,
2369 ))
2370 .await?;
2371 let authorization = DeviceJoinChallengePublicationAuthorization {
2372 attempt: bootstrap.publication_authorization.attempt.clone(),
2373 attempt_activation: bootstrap
2374 .publication_authorization
2375 .attempt_activation
2376 .clone(),
2377 };
2378 let published = Box::pin(crate::sync::provider::publish_cross_principal_challenge(
2379 storage,
2380 exact,
2381 db,
2382 &authorization,
2383 challenge,
2384 &context,
2385 &offer.provider,
2386 &owner,
2387 &activation_author,
2388 &administrator.device_signing_pubkey,
2389 ))
2390 .await
2391 .map_err(provider_error)?;
2392 DeviceProviderChallengePublication::CrossPrincipal {
2393 challenge: published,
2394 }
2395 }
2396 };
2397 let attempt_id = offer.attempt_id;
2398 let ready = ProviderReadyDeviceBootstrap {
2399 bootstrap: Box::new(bootstrap),
2400 challenge_publication,
2401 };
2402 if let Some(current) = Box::pin(load_store_journal(
2403 db,
2404 attempt_id,
2405 DeviceJoinRole::ProviderAdministrator,
2406 ))
2407 .await?
2408 {
2409 match &*current.progress {
2410 DeviceJoinRoleProgress::ProviderAdministrator(
2411 ProviderAdminJoinProgress::ProviderReady(existing),
2412 ) if existing == &ready => return Ok(ready),
2413 DeviceJoinRoleProgress::ProviderAdministrator(
2414 ProviderAdminJoinProgress::ApprovalPrepared(approval),
2415 ) if approval == &*ready.bootstrap.request.approval => {
2416 let observed = DeviceJoinJournalRecord {
2417 attempt_id,
2418 progress: Box::new(DeviceJoinRoleProgress::ProviderAdministrator(
2419 ProviderAdminJoinProgress::AttemptObserved(*ready.bootstrap.clone()),
2420 )),
2421 };
2422 Box::pin(advance_store_journal(db, ¤t, observed.clone())).await?;
2423 let intent = DeviceJoinJournalRecord {
2424 attempt_id,
2425 progress: Box::new(DeviceJoinRoleProgress::ProviderAdministrator(
2426 ProviderAdminJoinProgress::ChallengeCreateIntent(*ready.bootstrap.clone()),
2427 )),
2428 };
2429 Box::pin(advance_store_journal(db, &observed, intent.clone())).await?;
2430 Box::pin(advance_store_journal(
2431 db,
2432 &intent,
2433 DeviceJoinJournalRecord {
2434 attempt_id,
2435 progress: Box::new(DeviceJoinRoleProgress::ProviderAdministrator(
2436 ProviderAdminJoinProgress::ProviderReady(ready.clone()),
2437 )),
2438 },
2439 ))
2440 .await?;
2441 }
2442 _ => return Err(DeviceJoinError::JournalConflict),
2443 }
2444 }
2445 Ok(ready)
2446}
2447
2448#[allow(clippy::too_many_arguments)]
2449pub async fn bootstrap_pending_device(
2450 db: &Database,
2451 pending: &DeviceJoinJournalDatabase,
2452 storage: &dyn SyncStorage,
2453 peer_exact: Option<&dyn ExactSlotStorage>,
2454 identity_signer: &UserKeypair,
2455 bootstrap: ProviderReadyDeviceBootstrap,
2456 published_at: &str,
2457) -> Result<DeviceJoinReadiness, DeviceJoinError> {
2458 let offer = &bootstrap.bootstrap.request.approval.request.offer;
2459 let attempt_owner = Box::pin(crate::sync::store_objects::load_registration_ref(
2460 storage,
2461 &offer.store_root,
2462 &offer.owner_registration,
2463 ))
2464 .await?
2465 .value;
2466 let administrator = Box::pin(crate::sync::store_objects::load_registration_ref(
2467 storage,
2468 &offer.store_root,
2469 &offer.provider_admin.administrator,
2470 ))
2471 .await?
2472 .value;
2473 let verified_attempt = Box::pin(crate::sync::store_objects::load_device_join_attempt_ref(
2474 storage,
2475 &offer.store_root,
2476 &bootstrap.bootstrap.publication_authorization.attempt,
2477 &attempt_owner,
2478 ))
2479 .await?;
2480 if verified_attempt.value.expected_registration
2481 != bootstrap.bootstrap.request.expected_registration
2482 {
2483 return Err(DeviceJoinError::AttemptMismatch);
2484 }
2485 let bootstrap_authorization =
2486 Box::pin(crate::sync::store_pull::load_device_join_authorization(
2487 storage,
2488 &offer.store_root,
2489 &verified_attempt.value.membership,
2490 ))
2491 .await?;
2492 let bootstrap_plan = Box::pin(crate::sync::store_pull::prepare_device_join_bootstrap(
2493 storage,
2494 &offer.store_root,
2495 &verified_attempt.value.bootstrap_cut,
2496 &bootstrap
2497 .bootstrap
2498 .publication_authorization
2499 .attempt_activation,
2500 &bootstrap_authorization,
2501 ))
2502 .await?;
2503 let proof = Box::pin(crate::sync::store_registration::bootstrap_pending_device(
2504 db,
2505 storage,
2506 identity_signer,
2507 bootstrap
2508 .bootstrap
2509 .publication_authorization
2510 .attempt
2511 .clone(),
2512 verified_attempt,
2513 bootstrap_plan,
2514 bootstrap
2515 .bootstrap
2516 .publication_authorization
2517 .attempt_activation
2518 .clone(),
2519 &attempt_owner,
2520 published_at,
2521 ))
2522 .await?;
2523 let provider = match (
2524 &bootstrap.bootstrap.request.approval.admission,
2525 &bootstrap.bootstrap.request.response,
2526 &bootstrap.challenge_publication,
2527 ) {
2528 (
2529 DeviceProviderAdmissionChallenge::SamePrincipal,
2530 DeviceProviderResponseReservation::SamePrincipal,
2531 DeviceProviderChallengePublication::SamePrincipal,
2532 ) => DeviceProviderReadiness::SamePrincipal,
2533 (
2534 DeviceProviderAdmissionChallenge::CrossPrincipal(challenge),
2535 DeviceProviderResponseReservation::CrossPrincipal { response_slot },
2536 DeviceProviderChallengePublication::CrossPrincipal {
2537 challenge: published,
2538 },
2539 ) if challenge == published => {
2540 let exact = peer_exact.ok_or(DeviceJoinError::ExactSlotStorageRequired)?;
2541 let context = crate::sync::provider::CrossPrincipalResponseContext {
2542 challenge: cross_challenge_context(&bootstrap.bootstrap.request.approval.request),
2543 expected_registration_hash: bootstrap
2544 .bootstrap
2545 .request
2546 .expected_registration
2547 .registration_hash(),
2548 response_slot: response_slot.clone(),
2549 };
2550 DeviceProviderReadiness::CrossPrincipal(
2551 Box::pin(crate::sync::provider::create_cross_principal_response(
2552 exact,
2553 challenge,
2554 &context,
2555 &offer.provider,
2556 &administrator.device_signing_pubkey,
2557 identity_signer,
2558 ))
2559 .await
2560 .map_err(provider_error)?,
2561 )
2562 }
2563 _ => return Err(DeviceJoinError::AttemptMismatch),
2564 };
2565 let readiness = DeviceJoinReadiness { proof, provider };
2566 let pending = pending.clone();
2567 let bootstrap = Box::new(bootstrap);
2568 let readiness = Box::new(readiness);
2569 tokio::task::spawn_blocking(move || record_joiner_readiness(&pending, *bootstrap, *readiness))
2570 .await
2571 .map_err(|error| {
2572 DeviceJoinError::Store(format!("joiner readiness journal task failed: {error}"))
2573 })?
2574}
2575
2576fn record_joiner_readiness(
2577 pending: &DeviceJoinJournalDatabase,
2578 bootstrap: ProviderReadyDeviceBootstrap,
2579 readiness: DeviceJoinReadiness,
2580) -> Result<DeviceJoinReadiness, DeviceJoinError> {
2581 let offer = &bootstrap.bootstrap.request.approval.request.offer;
2582 let current = pending
2583 .load(offer.attempt_id, DeviceJoinRole::Joiner)?
2584 .ok_or(DeviceJoinError::JournalConflict)?;
2585 let prepared = match &*current.progress {
2586 DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::RegistrationPrepared(request))
2587 if request == &*bootstrap.bootstrap.request =>
2588 {
2589 current
2590 }
2591 DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::Ready(existing))
2592 if existing == &readiness =>
2593 {
2594 return Ok(readiness)
2595 }
2596 _ => return Err(DeviceJoinError::JournalConflict),
2597 };
2598 let provider_ready = DeviceJoinJournalRecord {
2599 attempt_id: offer.attempt_id,
2600 progress: Box::new(DeviceJoinRoleProgress::Joiner(
2601 JoinerJoinProgress::ProviderReady(bootstrap.clone()),
2602 )),
2603 };
2604 pending.advance(&prepared, provider_ready.clone())?;
2605 let registration_intent = DeviceJoinJournalRecord {
2606 attempt_id: offer.attempt_id,
2607 progress: Box::new(DeviceJoinRoleProgress::Joiner(
2608 JoinerJoinProgress::RegistrationCreateIntent(bootstrap.clone()),
2609 )),
2610 };
2611 pending.advance(&provider_ready, registration_intent.clone())?;
2612 let registration_created = DeviceJoinJournalRecord {
2613 attempt_id: offer.attempt_id,
2614 progress: Box::new(DeviceJoinRoleProgress::Joiner(
2615 JoinerJoinProgress::RegistrationCreated(readiness.proof.registration.clone()),
2616 )),
2617 };
2618 pending.advance(®istration_intent, registration_created.clone())?;
2619 let ack_intent = DeviceJoinJournalRecord {
2620 attempt_id: offer.attempt_id,
2621 progress: Box::new(DeviceJoinRoleProgress::Joiner(
2622 JoinerJoinProgress::AckCreateIntent(readiness.proof.registration.clone()),
2623 )),
2624 };
2625 pending.advance(®istration_created, ack_intent.clone())?;
2626 let ack_created = DeviceJoinJournalRecord {
2627 attempt_id: offer.attempt_id,
2628 progress: Box::new(DeviceJoinRoleProgress::Joiner(
2629 JoinerJoinProgress::AckCreated(readiness.proof.initial_ack.clone()),
2630 )),
2631 };
2632 pending.advance(&ack_intent, ack_created.clone())?;
2633 let ready_record = DeviceJoinJournalRecord {
2634 attempt_id: offer.attempt_id,
2635 progress: Box::new(DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::Ready(
2636 readiness.clone(),
2637 ))),
2638 };
2639 match readiness.provider {
2640 DeviceProviderReadiness::SamePrincipal => pending.advance(&ack_created, ready_record)?,
2641 DeviceProviderReadiness::CrossPrincipal(_) => {
2642 let response_intent = DeviceJoinJournalRecord {
2643 attempt_id: offer.attempt_id,
2644 progress: Box::new(DeviceJoinRoleProgress::Joiner(
2645 JoinerJoinProgress::ResponseCreateIntent(readiness.clone()),
2646 )),
2647 };
2648 pending.advance(&ack_created, response_intent.clone())?;
2649 pending.advance(&response_intent, ready_record)?;
2650 }
2651 }
2652 Ok(readiness)
2653}
2654
2655fn cross_challenge_context(
2656 request: &DeviceProviderAccessRequest,
2657) -> crate::sync::provider::CrossPrincipalChallengeContext {
2658 crate::sync::provider::CrossPrincipalChallengeContext {
2659 root: request.offer.store_root.clone(),
2660 attempt_id: request.offer.attempt_id,
2661 access_request_hash: request.request_hash(),
2662 provider_admin_grant: request.offer.provider_admin.grant_id.clone(),
2663 owner_registration: request.offer.owner_registration.clone(),
2664 member_pubkey: request.offer.member_pubkey.clone(),
2665 administrator_binding: request.offer.provider_admin.provider.clone(),
2666 peer_binding: request.peer_provider.clone(),
2667 }
2668}
2669
2670pub async fn complete_device_provider_admission(
2671 db: &Database,
2672 administrator_exact: Option<&dyn ExactSlotStorage>,
2673 identity_signer: &UserKeypair,
2674 readiness: DeviceJoinReadiness,
2675) -> Result<DeviceProviderAdmissionCompletion, DeviceJoinError> {
2676 let attempt_id = readiness.proof.attempt.attempt_id;
2677 let current = load_store_journal(db, attempt_id, DeviceJoinRole::ProviderAdministrator)
2678 .await?
2679 .ok_or(DeviceJoinError::JournalConflict)?;
2680 if let DeviceJoinRoleProgress::ProviderAdministrator(ProviderAdminJoinProgress::Completed(
2681 existing,
2682 )) = &*current.progress
2683 {
2684 if *existing.readiness == readiness {
2685 return Ok(existing.clone());
2686 }
2687 return Err(DeviceJoinError::JournalConflict);
2688 }
2689 let bootstrap = match &*current.progress {
2690 DeviceJoinRoleProgress::ProviderAdministrator(
2691 ProviderAdminJoinProgress::ProviderReady(bootstrap),
2692 ) => bootstrap.clone(),
2693 _ => return Err(DeviceJoinError::JournalConflict),
2694 };
2695 if readiness.proof.attempt != bootstrap.bootstrap.publication_authorization.attempt {
2696 return Err(DeviceJoinError::AttemptMismatch);
2697 }
2698 let offer = &bootstrap.bootstrap.request.approval.request.offer;
2699 let administrator = db
2700 .activated_store_device_registration(offer.provider_admin.administrator.clone())
2701 .await
2702 .map_err(database_error)?;
2703 let administrator_signer = administrator.device_signer(identity_signer)?;
2704 let admission = match (
2705 &bootstrap.bootstrap.request.approval.admission,
2706 &bootstrap.bootstrap.request.response,
2707 &readiness.provider,
2708 ) {
2709 (
2710 DeviceProviderAdmissionChallenge::SamePrincipal,
2711 DeviceProviderResponseReservation::SamePrincipal,
2712 DeviceProviderReadiness::SamePrincipal,
2713 ) => DeviceProviderAdmission::SamePrincipal,
2714 (
2715 DeviceProviderAdmissionChallenge::CrossPrincipal(challenge),
2716 DeviceProviderResponseReservation::CrossPrincipal { response_slot },
2717 DeviceProviderReadiness::CrossPrincipal(response),
2718 ) => {
2719 let exact = administrator_exact.ok_or(DeviceJoinError::ExactSlotStorageRequired)?;
2720 let context = crate::sync::provider::CrossPrincipalResponseContext {
2721 challenge: cross_challenge_context(&bootstrap.bootstrap.request.approval.request),
2722 expected_registration_hash: bootstrap
2723 .bootstrap
2724 .request
2725 .expected_registration
2726 .registration_hash(),
2727 response_slot: response_slot.clone(),
2728 };
2729 DeviceProviderAdmission::CrossPrincipal(
2730 crate::sync::provider::complete_cross_principal_probe(
2731 exact,
2732 db,
2733 challenge,
2734 response,
2735 &context,
2736 &offer.provider,
2737 &administrator_signer,
2738 &offer.member_pubkey,
2739 )
2740 .await
2741 .map_err(provider_error)?,
2742 )
2743 }
2744 _ => return Err(DeviceJoinError::AttemptMismatch),
2745 };
2746 let completion = DeviceProviderAdmissionCompletion {
2747 readiness: Box::new(readiness.clone()),
2748 admission,
2749 };
2750 let observed = DeviceJoinJournalRecord {
2751 attempt_id,
2752 progress: Box::new(DeviceJoinRoleProgress::ProviderAdministrator(
2753 ProviderAdminJoinProgress::ResponseObserved(readiness),
2754 )),
2755 };
2756 advance_store_journal(db, ¤t, observed.clone()).await?;
2757 advance_store_journal(
2758 db,
2759 &observed,
2760 DeviceJoinJournalRecord {
2761 attempt_id,
2762 progress: Box::new(DeviceJoinRoleProgress::ProviderAdministrator(
2763 ProviderAdminJoinProgress::Completed(completion.clone()),
2764 )),
2765 },
2766 )
2767 .await?;
2768 Ok(completion)
2769}
2770
2771#[allow(clippy::too_many_arguments)]
2772pub async fn cancel_device_join(
2773 db: &Database,
2774 storage: &dyn SyncStorage,
2775 coordination: Option<&dyn CoordinationStorage>,
2776 authorization: &DeviceJoinAuthorization,
2777 identity_signer: &UserKeypair,
2778 attempt_ref: DeviceJoinAttemptRef,
2779) -> Result<DeviceJoinCancellation, DeviceJoinError> {
2780 require_authorization_policy(db, storage, authorization).await?;
2781 let current = load_store_journal(db, attempt_ref.attempt_id, DeviceJoinRole::Owner)
2782 .await?
2783 .ok_or(DeviceJoinError::JournalConflict)?;
2784 if let DeviceJoinRoleProgress::Owner(OwnerJoinProgress::Cancelled(existing)) =
2785 &*current.progress
2786 {
2787 if existing.outcome.attempt() == &attempt_ref {
2788 return Ok(existing.clone());
2789 }
2790 return Err(DeviceJoinError::JournalConflict);
2791 }
2792 let expected_attempt = match &*current.progress {
2793 DeviceJoinRoleProgress::Owner(OwnerJoinProgress::AttemptActivated(bootstrap)) => {
2794 &bootstrap.publication_authorization.attempt
2795 }
2796 DeviceJoinRoleProgress::Owner(OwnerJoinProgress::ProviderReady(bootstrap)) => {
2797 &bootstrap.bootstrap.publication_authorization.attempt
2798 }
2799 DeviceJoinRoleProgress::Owner(OwnerJoinProgress::AdmissionCompleted(completion)) => {
2800 &completion.readiness.proof.attempt
2801 }
2802 DeviceJoinRoleProgress::Owner(OwnerJoinProgress::CancellationCreateIntent {
2803 attempt,
2804 ..
2805 }) => attempt,
2806 _ => return Err(DeviceJoinError::JournalConflict),
2807 };
2808 if expected_attempt != &attempt_ref {
2809 return Err(DeviceJoinError::AttemptMismatch);
2810 }
2811 let root = db
2812 .local_store_root_ref()
2813 .await
2814 .map_err(database_error)?
2815 .ok_or(DeviceJoinError::ActiveDeviceRequired)?;
2816 let attempt_context = crate::sync::storage::ProtocolObjectContext::store(
2817 root.store_root_hash,
2818 crate::sync::storage::ProtocolObjectDomain::DeviceJoinAttempt,
2819 );
2820 let attempt_prefix =
2821 crate::sync::store_commit::device_join_attempt_semantic_prefix(attempt_ref.attempt_id);
2822 let attempt_bytes = storage
2823 .read_protocol_object(&attempt_context, &attempt_ref.object, &attempt_prefix)
2824 .await?;
2825 let unverified_attempt: DeviceJoinAttempt = serde_json::from_slice(&attempt_bytes)?;
2826 let owner = db
2827 .activated_store_device_registration(unverified_attempt.owner_registration.clone())
2828 .await
2829 .map_err(database_error)?;
2830 let attempt = crate::sync::store_objects::load_device_join_attempt_ref(
2831 storage,
2832 &root,
2833 &attempt_ref,
2834 &owner,
2835 )
2836 .await?
2837 .value;
2838 let local_device_id = db
2839 .get_protocol_state(crate::database::LOCAL_DEVICE_ID_STATE_KEY)
2840 .await
2841 .map_err(database_error)?
2842 .ok_or(DeviceJoinError::ActiveDeviceRequired)?;
2843 if owner.device_id.to_string() != local_device_id
2844 || !authorization.is_owner_now(&keys::public_key_hex(identity_signer))
2845 {
2846 return Err(DeviceJoinError::OwnerAuthorityRequired);
2847 }
2848 let owner_signer = owner.device_signer(identity_signer)?;
2849 let outcome = crate::sync::store_commit::DeviceJoinOutcome::signed(
2850 attempt_ref.clone(),
2851 crate::sync::store_commit::DeviceJoinOutcomeBody::Cancelled,
2852 attempt.owner_registration.clone(),
2853 attempt.owner_grant.clone(),
2854 &owner,
2855 &owner_signer,
2856 )?;
2857 let context = crate::sync::storage::ProtocolObjectContext::store(
2858 root.store_root_hash,
2859 crate::sync::storage::ProtocolObjectDomain::DeviceJoinOutcome,
2860 );
2861 let prefix =
2862 crate::sync::store_commit::device_join_outcome_semantic_prefix(attempt_ref.attempt_id);
2863 let prepared = storage.prepare_protocol_object(
2864 &context,
2865 attempt.outcome_slot.clone(),
2866 &prefix,
2867 outcome.to_bytes(),
2868 )?;
2869 let outcome_ref = DeviceJoinOutcomeRef::Cancelled {
2870 attempt: attempt_ref.clone(),
2871 outcome_hash: outcome.outcome_hash(),
2872 object: prepared.reference().clone(),
2873 };
2874 let intent = DeviceJoinJournalRecord {
2875 attempt_id: attempt_ref.attempt_id,
2876 progress: Box::new(DeviceJoinRoleProgress::Owner(
2877 OwnerJoinProgress::CancellationCreateIntent {
2878 attempt: attempt_ref.clone(),
2879 cancellation: outcome_ref.clone(),
2880 prepared: PreparedDeviceJoinObject::from_prepared(&prepared),
2881 },
2882 )),
2883 };
2884 match &*current.progress {
2885 DeviceJoinRoleProgress::Owner(OwnerJoinProgress::AttemptActivated(_))
2886 | DeviceJoinRoleProgress::Owner(OwnerJoinProgress::ProviderReady(_))
2887 | DeviceJoinRoleProgress::Owner(OwnerJoinProgress::AdmissionCompleted(_)) => {
2888 advance_store_journal(db, ¤t, intent.clone()).await?;
2889 }
2890 DeviceJoinRoleProgress::Owner(OwnerJoinProgress::CancellationCreateIntent {
2891 attempt,
2892 cancellation,
2893 prepared: durable_prepared,
2894 }) if attempt == &attempt_ref
2895 && cancellation == &outcome_ref
2896 && durable_prepared == &PreparedDeviceJoinObject::from_prepared(&prepared) => {}
2897 _ => return Err(DeviceJoinError::JournalConflict),
2898 }
2899 storage.create_protocol_object(&prepared).await?;
2900 let opened = storage
2901 .read_protocol_object(&context, prepared.reference(), &prefix)
2902 .await?;
2903 if opened != outcome.to_bytes() {
2904 return Err(DeviceJoinError::AttemptMismatch);
2905 }
2906 let verified_outcome = crate::sync::store_objects::load_device_join_outcome_ref(
2907 storage,
2908 &root,
2909 &outcome_ref,
2910 &owner,
2911 )
2912 .await?;
2913 if verified_outcome.value != outcome {
2914 return Err(DeviceJoinError::AttemptMismatch);
2915 }
2916 let plan = crate::sync::store_outbound::prepare_device_join_commit(
2917 db,
2918 storage,
2919 coordination,
2920 &local_device_id,
2921 identity_signer,
2922 authorization.merge_chain(),
2923 )
2924 .await?;
2925 let outcome_activation = crate::sync::store_outbound::activate_device_join_commit(
2926 db,
2927 storage,
2928 coordination,
2929 plan,
2930 crate::sync::store_outbound::DeviceJoinStoreBatch::Outcome {
2931 outcome: outcome_ref.clone(),
2932 registration: None,
2933 },
2934 )
2935 .await?;
2936 let cancellation = DeviceJoinCancellation {
2937 outcome: outcome_ref,
2938 outcome_activation,
2939 };
2940 advance_store_journal(
2941 db,
2942 &intent,
2943 DeviceJoinJournalRecord {
2944 attempt_id: attempt_ref.attempt_id,
2945 progress: Box::new(DeviceJoinRoleProgress::Owner(OwnerJoinProgress::Cancelled(
2946 cancellation.clone(),
2947 ))),
2948 },
2949 )
2950 .await?;
2951 Ok(cancellation)
2952}
2953
2954#[allow(clippy::too_many_arguments)]
2955pub async fn prepare_device_join_cleanup(
2956 db: &Database,
2957 storage: &dyn SyncStorage,
2958 coordination: Option<&dyn CoordinationStorage>,
2959 executor_exact: &dyn ExactSlotStorage,
2960 authorization: &DeviceJoinAuthorization,
2961 identity_signer: &UserKeypair,
2962 cancellation: DeviceJoinCancellation,
2963 administrator_terminal: ProviderAdminJoinTerminal,
2964 joiner_terminal: JoinerJoinTerminal,
2965) -> Result<DeviceJoinCleanupReceipt, DeviceJoinError> {
2966 require_authorization_policy(db, storage, authorization).await?;
2967 require_cancelled_outcome(&cancellation.outcome)?;
2968 let attempt_ref = cancellation.outcome.attempt().clone();
2969 let current = load_store_journal(db, attempt_ref.attempt_id, DeviceJoinRole::Owner)
2970 .await?
2971 .ok_or(DeviceJoinError::JournalConflict)?;
2972 if let DeviceJoinRoleProgress::Owner(OwnerJoinProgress::CleanupReceipt(existing)) =
2973 &*current.progress
2974 {
2975 return Ok(existing.clone());
2976 }
2977 let durable_cancellation = match &*current.progress {
2978 DeviceJoinRoleProgress::Owner(OwnerJoinProgress::Cancelled(durable)) => durable,
2979 DeviceJoinRoleProgress::Owner(OwnerJoinProgress::CleanupReceiptCreateIntent {
2980 cancellation: durable,
2981 ..
2982 }) => durable,
2983 _ => return Err(DeviceJoinError::JournalConflict),
2984 };
2985 if durable_cancellation != &cancellation {
2986 return Err(DeviceJoinError::JournalConflict);
2987 }
2988 let root = db
2989 .local_store_root_ref()
2990 .await
2991 .map_err(database_error)?
2992 .ok_or(DeviceJoinError::ActiveDeviceRequired)?;
2993 let attempt_context = crate::sync::storage::ProtocolObjectContext::store(
2994 root.store_root_hash,
2995 crate::sync::storage::ProtocolObjectDomain::DeviceJoinAttempt,
2996 );
2997 let attempt_prefix =
2998 crate::sync::store_commit::device_join_attempt_semantic_prefix(attempt_ref.attempt_id);
2999 let attempt_bytes = storage
3000 .read_protocol_object(&attempt_context, &attempt_ref.object, &attempt_prefix)
3001 .await?;
3002 let unverified_attempt: DeviceJoinAttempt = serde_json::from_slice(&attempt_bytes)?;
3003 let owner = db
3004 .activated_store_device_registration(unverified_attempt.owner_registration.clone())
3005 .await
3006 .map_err(database_error)?;
3007 let attempt = crate::sync::store_objects::load_device_join_attempt_ref(
3008 storage,
3009 &root,
3010 &attempt_ref,
3011 &owner,
3012 )
3013 .await?
3014 .value;
3015 let outcome = crate::sync::store_objects::load_device_join_outcome_ref(
3016 storage,
3017 &root,
3018 &cancellation.outcome,
3019 &owner,
3020 )
3021 .await?
3022 .value;
3023 if !matches!(
3024 outcome.body,
3025 crate::sync::store_commit::DeviceJoinOutcomeBody::Cancelled
3026 ) {
3027 return Err(DeviceJoinError::AttemptMismatch);
3028 }
3029 validate_terminals(
3030 &cancellation.outcome,
3031 &administrator_terminal,
3032 &joiner_terminal,
3033 )?;
3034 verify_cleanup_terminals(db, &administrator_terminal, &joiner_terminal).await?;
3035 let local_device_id = db
3036 .get_protocol_state(crate::database::LOCAL_DEVICE_ID_STATE_KEY)
3037 .await
3038 .map_err(database_error)?
3039 .ok_or(DeviceJoinError::ActiveDeviceRequired)?;
3040 let (local_root, executor_ref, executor, executor_signer) =
3041 crate::sync::store_outbound::load_local_store_authority(
3042 db,
3043 &local_device_id,
3044 identity_signer,
3045 )
3046 .await?;
3047 if local_root != root || !authorization.is_owner_now(&executor.author_pubkey) {
3048 return Err(DeviceJoinError::OwnerAuthorityRequired);
3049 }
3050 let effective_executor = authorization.resolved_provider_admin(
3051 &attempt
3052 .provider_approval
3053 .request
3054 .offer
3055 .provider_admin
3056 .grant_id,
3057 )?;
3058 if effective_executor != *attempt.provider_approval.request.offer.provider_admin
3059 || effective_executor.administrator != executor_ref
3060 || effective_executor.provider != executor.provider
3061 {
3062 return Err(DeviceJoinError::ProviderAdministratorRequired);
3063 }
3064 let plan = crate::sync::store_outbound::prepare_device_join_commit(
3065 db,
3066 storage,
3067 coordination,
3068 &local_device_id,
3069 identity_signer,
3070 authorization.merge_chain(),
3071 )
3072 .await?;
3073 let deleted_slots = canonical_cleanup_slots(&attempt)?;
3074 let receipt_object = DeviceJoinCleanupReceiptObject::signed(
3075 &attempt,
3076 cancellation.outcome.clone(),
3077 administrator_terminal,
3078 joiner_terminal,
3079 deleted_slots.clone(),
3080 plan.membership_state().clone(),
3081 attempt
3082 .provider_approval
3083 .request
3084 .offer
3085 .provider_admin
3086 .grant_id
3087 .clone(),
3088 executor_ref,
3089 &executor,
3090 &executor_signer,
3091 )?;
3092 let context = crate::sync::storage::ProtocolObjectContext::store(
3093 root.store_root_hash,
3094 crate::sync::storage::ProtocolObjectDomain::DeviceJoinCleanupReceipt,
3095 );
3096 let prefix = crate::sync::store_commit::device_join_cleanup_receipt_semantic_prefix(
3097 attempt_ref.attempt_id,
3098 );
3099 let slot = storage
3100 .allocate_protocol_slot(&context, &prefix, ".json")
3101 .await?;
3102 let prepared =
3103 storage.prepare_protocol_object(&context, slot, &prefix, receipt_object.to_bytes())?;
3104 let receipt_ref = DeviceJoinCleanupReceiptRef {
3105 attempt_id: attempt_ref.attempt_id,
3106 receipt_hash: receipt_object.receipt_hash(),
3107 object: prepared.reference().clone(),
3108 };
3109 let intent = DeviceJoinJournalRecord {
3110 attempt_id: attempt_ref.attempt_id,
3111 progress: Box::new(DeviceJoinRoleProgress::Owner(
3112 OwnerJoinProgress::CleanupReceiptCreateIntent {
3113 cancellation: cancellation.clone(),
3114 receipt: receipt_ref.clone(),
3115 prepared: PreparedDeviceJoinObject::from_prepared(&prepared),
3116 },
3117 )),
3118 };
3119 match &*current.progress {
3120 DeviceJoinRoleProgress::Owner(OwnerJoinProgress::Cancelled(_)) => {
3121 advance_store_journal(db, ¤t, intent.clone()).await?;
3122 }
3123 DeviceJoinRoleProgress::Owner(OwnerJoinProgress::CleanupReceiptCreateIntent {
3124 receipt,
3125 prepared: durable_prepared,
3126 ..
3127 }) if receipt == &receipt_ref
3128 && durable_prepared == &PreparedDeviceJoinObject::from_prepared(&prepared) => {}
3129 _ => return Err(DeviceJoinError::JournalConflict),
3130 }
3131 for slot in &deleted_slots {
3132 ensure_exact_slot_absent(executor_exact, slot).await?;
3133 }
3134 storage.create_protocol_object(&prepared).await?;
3135 let opened = storage
3136 .read_protocol_object(&context, prepared.reference(), &prefix)
3137 .await?;
3138 if opened != receipt_object.to_bytes() {
3139 return Err(DeviceJoinError::CleanupMismatch);
3140 }
3141 receipt_ref.verify(&receipt_object, &executor)?;
3142 let receipt = DeviceJoinCleanupReceipt {
3143 receipt: receipt_ref,
3144 };
3145 advance_store_journal(
3146 db,
3147 &intent,
3148 DeviceJoinJournalRecord {
3149 attempt_id: attempt_ref.attempt_id,
3150 progress: Box::new(DeviceJoinRoleProgress::Owner(
3151 OwnerJoinProgress::CleanupReceipt(receipt.clone()),
3152 )),
3153 },
3154 )
3155 .await?;
3156 Ok(receipt)
3157}
3158
3159pub async fn close_device_provider_admission(
3160 db: &Database,
3161 storage: &dyn SyncStorage,
3162 administrator_exact: Option<&dyn ExactSlotStorage>,
3163 identity_signer: &UserKeypair,
3164 cancellation: DeviceJoinCancellation,
3165) -> Result<ProviderAdminJoinTerminal, DeviceJoinError> {
3166 require_cancelled_outcome(&cancellation.outcome)?;
3167 let attempt_ref = cancellation.outcome.attempt().clone();
3168 let current = load_store_journal(
3169 db,
3170 attempt_ref.attempt_id,
3171 DeviceJoinRole::ProviderAdministrator,
3172 )
3173 .await?
3174 .ok_or(DeviceJoinError::JournalConflict)?;
3175 match &*current.progress {
3176 DeviceJoinRoleProgress::ProviderAdministrator(ProviderAdminJoinProgress::Completed(
3177 completion,
3178 )) => return Ok(ProviderAdminJoinTerminal::Completed(completion.clone())),
3179 DeviceJoinRoleProgress::ProviderAdministrator(ProviderAdminJoinProgress::Cancelled(
3180 closure,
3181 )) => return Ok(ProviderAdminJoinTerminal::Cancelled(closure.clone())),
3182 DeviceJoinRoleProgress::ProviderAdministrator(ProviderAdminJoinProgress::WriteRevoked(
3183 revocation,
3184 )) => return Ok(ProviderAdminJoinTerminal::WriteRevoked(revocation.clone())),
3185 _ => {}
3186 }
3187 let root = db
3188 .local_store_root_ref()
3189 .await
3190 .map_err(database_error)?
3191 .ok_or(DeviceJoinError::ActiveDeviceRequired)?;
3192 let attempt_context = crate::sync::storage::ProtocolObjectContext::store(
3193 root.store_root_hash,
3194 crate::sync::storage::ProtocolObjectDomain::DeviceJoinAttempt,
3195 );
3196 let attempt_prefix =
3197 crate::sync::store_commit::device_join_attempt_semantic_prefix(attempt_ref.attempt_id);
3198 let attempt_bytes = storage
3199 .read_protocol_object(&attempt_context, &attempt_ref.object, &attempt_prefix)
3200 .await?;
3201 let unverified_attempt: DeviceJoinAttempt = serde_json::from_slice(&attempt_bytes)?;
3202 let owner = db
3203 .activated_store_device_registration(unverified_attempt.owner_registration.clone())
3204 .await
3205 .map_err(database_error)?;
3206 let attempt = crate::sync::store_objects::load_device_join_attempt_ref(
3207 storage,
3208 &root,
3209 &attempt_ref,
3210 &owner,
3211 )
3212 .await?
3213 .value;
3214 let outcome = crate::sync::store_objects::load_device_join_outcome_ref(
3215 storage,
3216 &root,
3217 &cancellation.outcome,
3218 &owner,
3219 )
3220 .await?
3221 .value;
3222 if !matches!(
3223 outcome.body,
3224 crate::sync::store_commit::DeviceJoinOutcomeBody::Cancelled
3225 ) {
3226 return Err(DeviceJoinError::AttemptMismatch);
3227 }
3228 let offer = &attempt.provider_approval.request.offer;
3229 let administrator = db
3230 .activated_store_device_registration(offer.provider_admin.administrator.clone())
3231 .await
3232 .map_err(database_error)?;
3233 let local_device_id = db
3234 .get_protocol_state(crate::database::LOCAL_DEVICE_ID_STATE_KEY)
3235 .await
3236 .map_err(database_error)?
3237 .ok_or(DeviceJoinError::ActiveDeviceRequired)?;
3238 if administrator.device_id.to_string() != local_device_id {
3239 return Err(DeviceJoinError::ProviderAdministratorRequired);
3240 }
3241 let administrator_signer = administrator.device_signer(identity_signer)?;
3242 let (challenge, prior_state_hash) = match &*current.progress {
3243 DeviceJoinRoleProgress::ProviderAdministrator(
3244 ProviderAdminJoinProgress::CleanupIntent {
3245 cancellation: durable,
3246 challenge,
3247 prior_state_hash,
3248 },
3249 ) if durable == &cancellation => (challenge.clone(), *prior_state_hash),
3250 DeviceJoinRoleProgress::ProviderAdministrator(
3251 ProviderAdminJoinProgress::ApprovalPrepared(_)
3252 | ProviderAdminJoinProgress::AttemptObserved(_)
3253 | ProviderAdminJoinProgress::ChallengeCreateIntent(_)
3254 | ProviderAdminJoinProgress::ProviderReady(_)
3255 | ProviderAdminJoinProgress::ResponseObserved(_),
3256 ) => {
3257 let challenge = match &attempt.provider_approval.admission {
3258 DeviceProviderAdmissionChallenge::SamePrincipal => {
3259 ProviderChallengeDisposition::SamePrincipal
3260 }
3261 DeviceProviderAdmissionChallenge::CrossPrincipal(challenge) => {
3262 let exact =
3263 administrator_exact.ok_or(DeviceJoinError::ExactSlotStorageRequired)?;
3264 match exact.read_at(&challenge.administrator_object.slot).await {
3265 Err(crate::storage::cloud::CloudHomeError::NotFound(_)) => {
3266 ProviderChallengeDisposition::NeverCreated
3267 }
3268 Ok(bytes) => {
3269 challenge
3270 .administrator_object
3271 .object
3272 .verify(&bytes)
3273 .map_err(|error| DeviceJoinError::Provider(error.to_string()))?;
3274 ProviderChallengeDisposition::Created(
3275 challenge.administrator_object.object.clone(),
3276 )
3277 }
3278 Err(error) => return Err(DeviceJoinError::Provider(error.to_string())),
3279 }
3280 }
3281 };
3282 let prior_state_hash = ObjectHash::digest(&serde_json::to_vec(¤t.progress)?);
3283 let intent = DeviceJoinJournalRecord {
3284 attempt_id: attempt_ref.attempt_id,
3285 progress: Box::new(DeviceJoinRoleProgress::ProviderAdministrator(
3286 ProviderAdminJoinProgress::CleanupIntent {
3287 cancellation: cancellation.clone(),
3288 challenge: challenge.clone(),
3289 prior_state_hash,
3290 },
3291 )),
3292 };
3293 advance_store_journal(db, ¤t, intent).await?;
3294 (challenge, prior_state_hash)
3295 }
3296 _ => return Err(DeviceJoinError::JournalConflict),
3297 };
3298 if let DeviceProviderAdmissionChallenge::CrossPrincipal(probe) =
3299 &attempt.provider_approval.admission
3300 {
3301 ensure_exact_slot_absent(
3302 administrator_exact.ok_or(DeviceJoinError::ExactSlotStorageRequired)?,
3303 &probe.administrator_object.slot,
3304 )
3305 .await?;
3306 }
3307 let closure = ProviderAdminJoinClosure::signed(
3308 cancellation.outcome,
3309 offer.provider_admin.administrator.clone(),
3310 challenge,
3311 prior_state_hash,
3312 &administrator,
3313 &administrator_signer,
3314 )?;
3315 let intent = load_store_journal(
3316 db,
3317 attempt_ref.attempt_id,
3318 DeviceJoinRole::ProviderAdministrator,
3319 )
3320 .await?
3321 .ok_or(DeviceJoinError::JournalConflict)?;
3322 advance_store_journal(
3323 db,
3324 &intent,
3325 DeviceJoinJournalRecord {
3326 attempt_id: attempt_ref.attempt_id,
3327 progress: Box::new(DeviceJoinRoleProgress::ProviderAdministrator(
3328 ProviderAdminJoinProgress::Cancelled(closure.clone()),
3329 )),
3330 },
3331 )
3332 .await?;
3333 Ok(ProviderAdminJoinTerminal::Cancelled(closure))
3334}
3335
3336pub async fn close_joining_device(
3337 pending: &DeviceJoinJournalDatabase,
3338 storage: &dyn SyncStorage,
3339 peer_exact: &dyn ExactSlotStorage,
3340 root: &StoreRootRef,
3341 identity_signer: &UserKeypair,
3342 cancellation: DeviceJoinCancellation,
3343) -> Result<JoinerJoinTerminal, DeviceJoinError> {
3344 require_cancelled_outcome(&cancellation.outcome)?;
3345 let attempt_ref = cancellation.outcome.attempt().clone();
3346 let current = pending
3347 .load(attempt_ref.attempt_id, DeviceJoinRole::Joiner)?
3348 .ok_or(DeviceJoinError::JournalConflict)?;
3349 match &*current.progress {
3350 DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::Cancelled(closure)) => {
3351 return Ok(JoinerJoinTerminal::Cancelled(closure.clone()));
3352 }
3353 DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::WriteRevoked(revocation)) => {
3354 return Ok(JoinerJoinTerminal::WriteRevoked(revocation.clone()));
3355 }
3356 _ => {}
3357 }
3358 let allowed = matches!(
3359 &*current.progress,
3360 DeviceJoinRoleProgress::Joiner(
3361 JoinerJoinProgress::RegistrationPrepared(_)
3362 | JoinerJoinProgress::ProviderReady(_)
3363 | JoinerJoinProgress::RegistrationCreateIntent(_)
3364 | JoinerJoinProgress::RegistrationCreated(_)
3365 | JoinerJoinProgress::AckCreateIntent(_)
3366 | JoinerJoinProgress::AckCreated(_)
3367 | JoinerJoinProgress::ResponseCreateIntent(_)
3368 | JoinerJoinProgress::Ready(_)
3369 | JoinerJoinProgress::CleanupIntent { .. }
3370 )
3371 );
3372 if !allowed {
3373 return Err(DeviceJoinError::JournalConflict);
3374 }
3375 let attempt_context = crate::sync::storage::ProtocolObjectContext::store(
3376 root.store_root_hash,
3377 crate::sync::storage::ProtocolObjectDomain::DeviceJoinAttempt,
3378 );
3379 let attempt_prefix =
3380 crate::sync::store_commit::device_join_attempt_semantic_prefix(attempt_ref.attempt_id);
3381 let attempt_bytes = storage
3382 .read_protocol_object(&attempt_context, &attempt_ref.object, &attempt_prefix)
3383 .await?;
3384 let unverified_attempt: DeviceJoinAttempt = serde_json::from_slice(&attempt_bytes)?;
3385 let owner = crate::sync::store_objects::load_registration_ref(
3386 storage,
3387 root,
3388 &unverified_attempt.owner_registration,
3389 )
3390 .await?
3391 .value;
3392 let attempt = crate::sync::store_objects::load_device_join_attempt_ref(
3393 storage,
3394 root,
3395 &attempt_ref,
3396 &owner,
3397 )
3398 .await?
3399 .value;
3400 let outcome = crate::sync::store_objects::load_device_join_outcome_ref(
3401 storage,
3402 root,
3403 &cancellation.outcome,
3404 &owner,
3405 )
3406 .await?
3407 .value;
3408 if !matches!(
3409 outcome.body,
3410 crate::sync::store_commit::DeviceJoinOutcomeBody::Cancelled
3411 ) {
3412 return Err(DeviceJoinError::AttemptMismatch);
3413 }
3414 let joining_device_signer = attempt
3415 .expected_registration
3416 .device_signer(identity_signer)?;
3417 let (registration, initial_ack, response, prior_state_hash, intent) = match &*current.progress {
3418 DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::CleanupIntent {
3419 cancellation: durable,
3420 registration,
3421 initial_ack,
3422 response,
3423 prior_state_hash,
3424 }) if durable == &cancellation => (
3425 registration.clone(),
3426 initial_ack.clone(),
3427 response.clone(),
3428 *prior_state_hash,
3429 current.clone(),
3430 ),
3431 _ => {
3432 let registration = observe_exact_slot(peer_exact, &attempt.registration_slot).await?;
3433 let initial_ack = observe_exact_slot(
3434 peer_exact,
3435 attempt.expected_registration.acknowledgements.first_slot(),
3436 )
3437 .await?;
3438 let response = match &attempt.provider_response {
3439 DeviceProviderResponseReservation::SamePrincipal => {
3440 JoinerResponseDisposition::SamePrincipal
3441 }
3442 DeviceProviderResponseReservation::CrossPrincipal { response_slot } => {
3443 JoinerResponseDisposition::Slot(
3444 observe_exact_slot(peer_exact, response_slot).await?,
3445 )
3446 }
3447 };
3448 let prior_state_hash = ObjectHash::digest(&serde_json::to_vec(¤t.progress)?);
3449 let intent = DeviceJoinJournalRecord {
3450 attempt_id: attempt_ref.attempt_id,
3451 progress: Box::new(DeviceJoinRoleProgress::Joiner(
3452 JoinerJoinProgress::CleanupIntent {
3453 cancellation: cancellation.clone(),
3454 registration: registration.clone(),
3455 initial_ack: initial_ack.clone(),
3456 response: response.clone(),
3457 prior_state_hash,
3458 },
3459 )),
3460 };
3461 pending.advance(¤t, intent.clone())?;
3462 (
3463 registration,
3464 initial_ack,
3465 response,
3466 prior_state_hash,
3467 intent,
3468 )
3469 }
3470 };
3471 for slot in canonical_cleanup_slots(&attempt)? {
3472 ensure_exact_slot_absent(peer_exact, &slot).await?;
3473 }
3474 let closure = JoinerJoinClosure::signed(
3475 cancellation.outcome,
3476 attempt.expected_registration,
3477 registration,
3478 initial_ack,
3479 response,
3480 prior_state_hash,
3481 &joining_device_signer,
3482 )?;
3483 pending.advance(
3484 &intent,
3485 DeviceJoinJournalRecord {
3486 attempt_id: attempt_ref.attempt_id,
3487 progress: Box::new(DeviceJoinRoleProgress::Joiner(
3488 JoinerJoinProgress::Cancelled(closure.clone()),
3489 )),
3490 },
3491 )?;
3492 Ok(JoinerJoinTerminal::Cancelled(closure))
3493}
3494
3495#[allow(clippy::too_many_arguments)]
3496async fn sign_device_join_producer_write_revocation(
3497 db: &Database,
3498 storage: &dyn SyncStorage,
3499 authorization: &DeviceJoinAuthorization,
3500 identity_signer: &UserKeypair,
3501 cancellation: DeviceJoinCancellation,
3502 producer: DeviceJoinProducer,
3503 withdrawal: ProviderAccessWithdrawal,
3504 executor_grant: ProviderAdminGrantId,
3505) -> Result<DeviceJoinProducerWriteRevocation, DeviceJoinError> {
3506 require_authorization_policy(db, storage, authorization).await?;
3507 require_cancelled_outcome(&cancellation.outcome)?;
3508 let attempt_ref = cancellation.outcome.attempt().clone();
3509 let root = db
3510 .local_store_root_ref()
3511 .await
3512 .map_err(database_error)?
3513 .ok_or(DeviceJoinError::ActiveDeviceRequired)?;
3514 let attempt_context = crate::sync::storage::ProtocolObjectContext::store(
3515 root.store_root_hash,
3516 crate::sync::storage::ProtocolObjectDomain::DeviceJoinAttempt,
3517 );
3518 let attempt_prefix =
3519 crate::sync::store_commit::device_join_attempt_semantic_prefix(attempt_ref.attempt_id);
3520 let attempt_bytes = storage
3521 .read_protocol_object(&attempt_context, &attempt_ref.object, &attempt_prefix)
3522 .await?;
3523 let unverified_attempt: DeviceJoinAttempt = serde_json::from_slice(&attempt_bytes)?;
3524 let owner = db
3525 .activated_store_device_registration(unverified_attempt.owner_registration.clone())
3526 .await
3527 .map_err(database_error)?;
3528 let attempt = crate::sync::store_objects::load_device_join_attempt_ref(
3529 storage,
3530 &root,
3531 &attempt_ref,
3532 &owner,
3533 )
3534 .await?
3535 .value;
3536 let outcome = crate::sync::store_objects::load_device_join_outcome_ref(
3537 storage,
3538 &root,
3539 &cancellation.outcome,
3540 &owner,
3541 )
3542 .await?
3543 .value;
3544 if !matches!(
3545 outcome.body,
3546 crate::sync::store_commit::DeviceJoinOutcomeBody::Cancelled
3547 ) {
3548 return Err(DeviceJoinError::AttemptMismatch);
3549 }
3550 let executor_admin = authorization.resolved_provider_admin(&executor_grant)?;
3551 let executor = db
3552 .activated_store_device_registration(executor_admin.administrator.clone())
3553 .await
3554 .map_err(database_error)?;
3555 let local_device_id = db
3556 .get_protocol_state(crate::database::LOCAL_DEVICE_ID_STATE_KEY)
3557 .await
3558 .map_err(database_error)?
3559 .ok_or(DeviceJoinError::ActiveDeviceRequired)?;
3560 if executor.device_id.to_string() != local_device_id {
3561 return Err(DeviceJoinError::ProviderAdministratorRequired);
3562 }
3563 let executor_signer = executor.device_signer(identity_signer)?;
3564 let (authority, protected_slots, locator) = match producer {
3565 DeviceJoinProducer::ProviderAdministrator => {
3566 let DeviceProviderAdmissionChallenge::CrossPrincipal(challenge) =
3567 &attempt.provider_approval.admission
3568 else {
3569 return Err(DeviceJoinError::CleanupMismatch);
3570 };
3571 (
3572 ProviderWriteAuthorityRef::ProviderAdministrator(
3573 attempt
3574 .provider_approval
3575 .request
3576 .offer
3577 .provider_admin
3578 .grant_id
3579 .clone(),
3580 ),
3581 vec![challenge.administrator_object.slot.clone()],
3582 &attempt
3583 .provider_approval
3584 .request
3585 .offer
3586 .provider_admin
3587 .access,
3588 )
3589 }
3590 DeviceJoinProducer::Joiner => {
3591 let mut slots = vec![
3592 attempt.registration_slot.clone(),
3593 attempt
3594 .expected_registration
3595 .acknowledgements
3596 .first_slot()
3597 .clone(),
3598 ];
3599 if let DeviceProviderResponseReservation::CrossPrincipal { response_slot } =
3600 &attempt.provider_response
3601 {
3602 slots.push(response_slot.clone());
3603 }
3604 (
3605 ProviderWriteAuthorityRef::MemberAccess(
3606 attempt.provider_approval.access_grant.grant_ref.clone(),
3607 ),
3608 slots,
3609 &attempt.provider_approval.access_grant.grant.locator,
3610 )
3611 }
3612 };
3613 if !withdrawal_matches_locator(&withdrawal, locator) {
3614 return Err(DeviceJoinError::CleanupMismatch);
3615 }
3616 DeviceJoinProducerWriteRevocation::signed(
3617 cancellation.outcome,
3618 producer,
3619 authority,
3620 protected_slots,
3621 withdrawal,
3622 executor_grant,
3623 executor_admin.administrator,
3624 &executor,
3625 &executor_signer,
3626 )
3627}
3628
3629#[allow(clippy::too_many_arguments)]
3630pub async fn revoke_device_provider_admission_writes(
3631 db: &Database,
3632 storage: &dyn SyncStorage,
3633 authorization: &DeviceJoinAuthorization,
3634 identity_signer: &UserKeypair,
3635 cancellation: DeviceJoinCancellation,
3636 withdrawal: ProviderAccessWithdrawal,
3637 executor_grant: ProviderAdminGrantId,
3638) -> Result<ProviderAdminJoinTerminal, DeviceJoinError> {
3639 let attempt_id = cancellation.outcome.attempt().attempt_id;
3640 let current = load_store_journal(db, attempt_id, DeviceJoinRole::ProviderAdministrator)
3641 .await?
3642 .ok_or(DeviceJoinError::JournalConflict)?;
3643 match &*current.progress {
3644 DeviceJoinRoleProgress::ProviderAdministrator(ProviderAdminJoinProgress::Completed(
3645 completion,
3646 )) => return Ok(ProviderAdminJoinTerminal::Completed(completion.clone())),
3647 DeviceJoinRoleProgress::ProviderAdministrator(ProviderAdminJoinProgress::Cancelled(
3648 closure,
3649 )) => return Ok(ProviderAdminJoinTerminal::Cancelled(closure.clone())),
3650 DeviceJoinRoleProgress::ProviderAdministrator(ProviderAdminJoinProgress::WriteRevoked(
3651 revocation,
3652 )) => return Ok(ProviderAdminJoinTerminal::WriteRevoked(revocation.clone())),
3653 DeviceJoinRoleProgress::ProviderAdministrator(
3654 ProviderAdminJoinProgress::ApprovalPrepared(_)
3655 | ProviderAdminJoinProgress::AttemptObserved(_)
3656 | ProviderAdminJoinProgress::ChallengeCreateIntent(_)
3657 | ProviderAdminJoinProgress::ProviderReady(_)
3658 | ProviderAdminJoinProgress::ResponseObserved(_)
3659 | ProviderAdminJoinProgress::CleanupIntent { .. },
3660 ) => {}
3661 _ => return Err(DeviceJoinError::JournalConflict),
3662 }
3663 let revocation = Box::pin(sign_device_join_producer_write_revocation(
3664 db,
3665 storage,
3666 authorization,
3667 identity_signer,
3668 cancellation,
3669 DeviceJoinProducer::ProviderAdministrator,
3670 withdrawal,
3671 executor_grant,
3672 ))
3673 .await?;
3674 advance_store_journal(
3675 db,
3676 ¤t,
3677 DeviceJoinJournalRecord {
3678 attempt_id,
3679 progress: Box::new(DeviceJoinRoleProgress::ProviderAdministrator(
3680 ProviderAdminJoinProgress::WriteRevoked(revocation.clone()),
3681 )),
3682 },
3683 )
3684 .await?;
3685 Ok(ProviderAdminJoinTerminal::WriteRevoked(revocation))
3686}
3687
3688#[allow(clippy::too_many_arguments)]
3689pub async fn revoke_joining_device_writes(
3690 pending: &DeviceJoinJournalDatabase,
3691 db: &Database,
3692 storage: &dyn SyncStorage,
3693 authorization: &DeviceJoinAuthorization,
3694 identity_signer: &UserKeypair,
3695 cancellation: DeviceJoinCancellation,
3696 withdrawal: ProviderAccessWithdrawal,
3697 executor_grant: ProviderAdminGrantId,
3698) -> Result<JoinerJoinTerminal, DeviceJoinError> {
3699 let attempt_id = cancellation.outcome.attempt().attempt_id;
3700 let current = pending
3701 .load(attempt_id, DeviceJoinRole::Joiner)?
3702 .ok_or(DeviceJoinError::JournalConflict)?;
3703 match &*current.progress {
3704 DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::Ready(readiness)) => {
3705 return Ok(JoinerJoinTerminal::Ready(readiness.clone()));
3706 }
3707 DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::Cancelled(closure)) => {
3708 return Ok(JoinerJoinTerminal::Cancelled(closure.clone()));
3709 }
3710 DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::WriteRevoked(revocation)) => {
3711 return Ok(JoinerJoinTerminal::WriteRevoked(revocation.clone()));
3712 }
3713 DeviceJoinRoleProgress::Joiner(
3714 JoinerJoinProgress::RegistrationPrepared(_)
3715 | JoinerJoinProgress::ProviderReady(_)
3716 | JoinerJoinProgress::RegistrationCreateIntent(_)
3717 | JoinerJoinProgress::RegistrationCreated(_)
3718 | JoinerJoinProgress::AckCreateIntent(_)
3719 | JoinerJoinProgress::AckCreated(_)
3720 | JoinerJoinProgress::ResponseCreateIntent(_)
3721 | JoinerJoinProgress::CleanupIntent { .. },
3722 ) => {}
3723 _ => return Err(DeviceJoinError::JournalConflict),
3724 }
3725 let revocation = Box::pin(sign_device_join_producer_write_revocation(
3726 db,
3727 storage,
3728 authorization,
3729 identity_signer,
3730 cancellation,
3731 DeviceJoinProducer::Joiner,
3732 withdrawal,
3733 executor_grant,
3734 ))
3735 .await?;
3736 pending.advance(
3737 ¤t,
3738 DeviceJoinJournalRecord {
3739 attempt_id,
3740 progress: Box::new(DeviceJoinRoleProgress::Joiner(
3741 JoinerJoinProgress::WriteRevoked(revocation.clone()),
3742 )),
3743 },
3744 )?;
3745 Ok(JoinerJoinTerminal::WriteRevoked(revocation))
3746}
3747
3748#[allow(clippy::too_many_arguments)]
3749pub async fn activate_device_join_cleanup(
3750 db: &Database,
3751 storage: &dyn SyncStorage,
3752 coordination: Option<&dyn CoordinationStorage>,
3753 authorization: &DeviceJoinAuthorization,
3754 identity_signer: &UserKeypair,
3755 attempt_id: DeviceJoinAttemptId,
3756 receipt: DeviceJoinCleanupReceipt,
3757) -> Result<DeviceJoinCleanupActivation, DeviceJoinError> {
3758 require_authorization_policy(db, storage, authorization).await?;
3759 let current = load_store_journal(db, attempt_id, DeviceJoinRole::Owner)
3760 .await?
3761 .ok_or(DeviceJoinError::JournalConflict)?;
3762 if let DeviceJoinRoleProgress::Owner(OwnerJoinProgress::CleanupActivated(existing)) =
3763 &*current.progress
3764 {
3765 if existing.receipt == receipt.receipt {
3766 return Ok(existing.clone());
3767 }
3768 return Err(DeviceJoinError::JournalConflict);
3769 }
3770 let DeviceJoinRoleProgress::Owner(OwnerJoinProgress::CleanupReceipt(durable)) =
3771 &*current.progress
3772 else {
3773 return Err(DeviceJoinError::JournalConflict);
3774 };
3775 if durable != &receipt {
3776 return Err(DeviceJoinError::JournalConflict);
3777 }
3778 let local_device_id = db
3779 .get_protocol_state(crate::database::LOCAL_DEVICE_ID_STATE_KEY)
3780 .await
3781 .map_err(database_error)?
3782 .ok_or(DeviceJoinError::ActiveDeviceRequired)?;
3783 let plan = crate::sync::store_outbound::prepare_device_join_commit(
3784 db,
3785 storage,
3786 coordination,
3787 &local_device_id,
3788 identity_signer,
3789 authorization.merge_chain(),
3790 )
3791 .await?;
3792 let root = db
3793 .local_store_root_ref()
3794 .await
3795 .map_err(database_error)?
3796 .ok_or(DeviceJoinError::ActiveDeviceRequired)?;
3797 let context = crate::sync::storage::ProtocolObjectContext::store(
3798 root.store_root_hash,
3799 crate::sync::storage::ProtocolObjectDomain::DeviceJoinCleanupReceipt,
3800 );
3801 let prefix = crate::sync::store_commit::device_join_cleanup_receipt_semantic_prefix(
3802 receipt.receipt.attempt_id,
3803 );
3804 let bytes = storage
3805 .read_protocol_object(&context, &receipt.receipt.object, &prefix)
3806 .await?;
3807 let receipt_object: DeviceJoinCleanupReceiptObject = serde_json::from_slice(&bytes)?;
3808 let executor = db
3809 .activated_store_device_registration(receipt_object.executor.clone())
3810 .await
3811 .map_err(database_error)?;
3812 receipt.receipt.verify(&receipt_object, &executor)?;
3813 if receipt_object.store_root_hash != root.store_root_hash
3814 || plan.membership_state() != &receipt_object.membership
3815 {
3816 return Err(DeviceJoinError::CleanupMismatch);
3817 }
3818 let activation_ref = crate::sync::store_outbound::activate_device_join_commit(
3819 db,
3820 storage,
3821 coordination,
3822 plan,
3823 crate::sync::store_outbound::DeviceJoinStoreBatch::CleanupReceipt(receipt.receipt.clone()),
3824 )
3825 .await?;
3826 let activation = DeviceJoinCleanupActivation {
3827 receipt: receipt.receipt,
3828 activation: activation_ref,
3829 };
3830 advance_store_journal(
3831 db,
3832 ¤t,
3833 DeviceJoinJournalRecord {
3834 attempt_id,
3835 progress: Box::new(DeviceJoinRoleProgress::Owner(
3836 OwnerJoinProgress::CleanupActivated(activation.clone()),
3837 )),
3838 },
3839 )
3840 .await?;
3841 Ok(activation)
3842}
3843
3844pub async fn complete_owner_device_join_cleanup(
3845 db: &Database,
3846 attempt_id: DeviceJoinAttemptId,
3847 activation: DeviceJoinCleanupActivation,
3848) -> Result<DeviceJoinCleanupActivation, DeviceJoinError> {
3849 let current = load_store_journal(db, attempt_id, DeviceJoinRole::Owner)
3850 .await?
3851 .ok_or(DeviceJoinError::JournalConflict)?;
3852 if let DeviceJoinRoleProgress::Owner(OwnerJoinProgress::CancelledComplete(existing)) =
3853 &*current.progress
3854 {
3855 if existing == &activation {
3856 return Ok(existing.clone());
3857 }
3858 return Err(DeviceJoinError::JournalConflict);
3859 }
3860 let DeviceJoinRoleProgress::Owner(OwnerJoinProgress::CleanupActivated(durable)) =
3861 &*current.progress
3862 else {
3863 return Err(DeviceJoinError::JournalConflict);
3864 };
3865 if durable != &activation {
3866 return Err(DeviceJoinError::JournalConflict);
3867 }
3868 advance_store_journal(
3869 db,
3870 ¤t,
3871 DeviceJoinJournalRecord {
3872 attempt_id,
3873 progress: Box::new(DeviceJoinRoleProgress::Owner(
3874 OwnerJoinProgress::CancelledComplete(activation.clone()),
3875 )),
3876 },
3877 )
3878 .await?;
3879 Ok(activation)
3880}
3881
3882pub fn complete_joiner_device_join_cleanup(
3883 pending: &DeviceJoinJournalDatabase,
3884 attempt_id: DeviceJoinAttemptId,
3885 activation: DeviceJoinCleanupActivation,
3886) -> Result<DeviceJoinCleanupActivation, DeviceJoinError> {
3887 let current = pending
3888 .load(attempt_id, DeviceJoinRole::Joiner)?
3889 .ok_or(DeviceJoinError::JournalConflict)?;
3890 if let DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::CancelledComplete(existing)) =
3891 &*current.progress
3892 {
3893 if existing == &activation {
3894 return Ok(existing.clone());
3895 }
3896 return Err(DeviceJoinError::JournalConflict);
3897 }
3898 let activated = DeviceJoinJournalRecord {
3899 attempt_id,
3900 progress: Box::new(DeviceJoinRoleProgress::Joiner(
3901 JoinerJoinProgress::CleanupActivated(activation.clone()),
3902 )),
3903 };
3904 match &*current.progress {
3905 DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::Cancelled(_))
3906 | DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::WriteRevoked(_)) => {
3907 pending.advance(¤t, activated.clone())?;
3908 }
3909 DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::CleanupActivated(durable))
3910 if durable == &activation => {}
3911 _ => return Err(DeviceJoinError::JournalConflict),
3912 }
3913 pending.advance(
3914 &activated,
3915 DeviceJoinJournalRecord {
3916 attempt_id,
3917 progress: Box::new(DeviceJoinRoleProgress::Joiner(
3918 JoinerJoinProgress::CancelledComplete(activation.clone()),
3919 )),
3920 },
3921 )?;
3922 Ok(activation)
3923}
3924
3925pub fn device_join_status(record: &DeviceJoinJournalRecord) -> DeviceJoinStatus {
3926 match &*record.progress {
3927 DeviceJoinRoleProgress::Owner(OwnerJoinProgress::Offered(offer))
3928 | DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::OfferReceived(offer)) => {
3929 DeviceJoinStatus::AwaitingAccessRequest {
3930 offer: offer.clone(),
3931 }
3932 }
3933 DeviceJoinRoleProgress::ProviderAdministrator(
3934 ProviderAdminJoinProgress::AccessRequested(request),
3935 )
3936 | DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::AccessRequested(request)) => {
3937 DeviceJoinStatus::AwaitingProviderAdmission {
3938 request: request.clone(),
3939 }
3940 }
3941 DeviceJoinRoleProgress::ProviderAdministrator(
3942 ProviderAdminJoinProgress::ApprovalPrepared(approval),
3943 )
3944 | DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::ApprovalReceived(approval)) => {
3945 DeviceJoinStatus::AwaitingRegistrationRequest {
3946 approval: approval.clone(),
3947 }
3948 }
3949 DeviceJoinRoleProgress::Owner(OwnerJoinProgress::RegistrationRequested(request))
3950 | DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::RegistrationPrepared(request)) => {
3951 DeviceJoinStatus::AwaitingBootstrap {
3952 request: request.clone(),
3953 }
3954 }
3955 DeviceJoinRoleProgress::Owner(OwnerJoinProgress::AttemptActivated(bootstrap))
3956 | DeviceJoinRoleProgress::ProviderAdministrator(
3957 ProviderAdminJoinProgress::AttemptObserved(bootstrap),
3958 )
3959 | DeviceJoinRoleProgress::ProviderAdministrator(
3960 ProviderAdminJoinProgress::ChallengeCreateIntent(bootstrap),
3961 ) => DeviceJoinStatus::AwaitingChallengePublication {
3962 bootstrap: bootstrap.clone(),
3963 },
3964 DeviceJoinRoleProgress::Owner(OwnerJoinProgress::ProviderReady(bootstrap))
3965 | DeviceJoinRoleProgress::ProviderAdministrator(
3966 ProviderAdminJoinProgress::ProviderReady(bootstrap),
3967 )
3968 | DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::ProviderReady(bootstrap))
3969 | DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::RegistrationCreateIntent(bootstrap)) => {
3970 DeviceJoinStatus::AwaitingReadiness {
3971 bootstrap: bootstrap.clone(),
3972 }
3973 }
3974 DeviceJoinRoleProgress::ProviderAdministrator(
3975 ProviderAdminJoinProgress::ResponseObserved(readiness),
3976 )
3977 | DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::Ready(readiness)) => {
3978 DeviceJoinStatus::AwaitingProviderCompletion {
3979 readiness: readiness.clone(),
3980 }
3981 }
3982 DeviceJoinRoleProgress::Owner(OwnerJoinProgress::AdmissionCompleted(completion))
3983 | DeviceJoinRoleProgress::ProviderAdministrator(ProviderAdminJoinProgress::Completed(
3984 completion,
3985 )) => DeviceJoinStatus::AwaitingActivation {
3986 completion: completion.clone(),
3987 },
3988 DeviceJoinRoleProgress::Owner(OwnerJoinProgress::ActivationPrepared(activation))
3989 | DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::ActivationObserved(activation)) => {
3990 DeviceJoinStatus::AwaitingCompletion {
3991 activation: activation.clone(),
3992 }
3993 }
3994 DeviceJoinRoleProgress::Owner(OwnerJoinProgress::Activated(store))
3995 | DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::Activated(store)) => {
3996 DeviceJoinStatus::Activated {
3997 store: store.clone(),
3998 }
3999 }
4000 DeviceJoinRoleProgress::Owner(OwnerJoinProgress::Abandoned(abandonment))
4001 | DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::Abandoned(abandonment)) => {
4002 DeviceJoinStatus::Abandoned {
4003 abandonment: abandonment.clone(),
4004 }
4005 }
4006 DeviceJoinRoleProgress::ProviderAdministrator(
4007 ProviderAdminJoinProgress::AccessGrantPrepared { request, grant, .. },
4008 ) => DeviceJoinStatus::ProviderAccessGrantCreatePending {
4009 request: request.clone(),
4010 grant: grant.clone(),
4011 },
4012 DeviceJoinRoleProgress::Owner(OwnerJoinProgress::AbandonmentCreateIntent {
4013 abandonment,
4014 ..
4015 }) => DeviceJoinStatus::AbandonmentCreatePending {
4016 abandonment: abandonment.clone(),
4017 },
4018 DeviceJoinRoleProgress::Owner(OwnerJoinProgress::CancellationCreateIntent {
4019 cancellation,
4020 ..
4021 }) => DeviceJoinStatus::CancellationCreatePending {
4022 cancellation: cancellation.clone(),
4023 },
4024 DeviceJoinRoleProgress::Owner(OwnerJoinProgress::Cancelled(cancellation)) => {
4025 DeviceJoinStatus::CleanupPending {
4026 cancellation: cancellation.clone(),
4027 progress: DeviceJoinCleanupProgress::AwaitingBoth,
4028 }
4029 }
4030 DeviceJoinRoleProgress::ProviderAdministrator(
4031 ProviderAdminJoinProgress::CleanupIntent { cancellation, .. },
4032 ) => DeviceJoinStatus::ProviderClosurePending {
4033 cancellation: cancellation.clone(),
4034 producer: DeviceJoinProducer::ProviderAdministrator,
4035 },
4036 DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::CleanupIntent {
4037 cancellation, ..
4038 }) => DeviceJoinStatus::ProviderClosurePending {
4039 cancellation: cancellation.clone(),
4040 producer: DeviceJoinProducer::Joiner,
4041 },
4042 DeviceJoinRoleProgress::ProviderAdministrator(ProviderAdminJoinProgress::Cancelled(
4043 closure,
4044 )) => DeviceJoinStatus::ProviderClosed {
4045 terminal: ProviderAdminJoinTerminal::Cancelled(closure.clone()),
4046 },
4047 DeviceJoinRoleProgress::ProviderAdministrator(ProviderAdminJoinProgress::WriteRevoked(
4048 revocation,
4049 )) => DeviceJoinStatus::ProviderClosed {
4050 terminal: ProviderAdminJoinTerminal::WriteRevoked(revocation.clone()),
4051 },
4052 DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::Cancelled(closure)) => {
4053 DeviceJoinStatus::JoinerClosed {
4054 terminal: JoinerJoinTerminal::Cancelled(closure.clone()),
4055 }
4056 }
4057 DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::WriteRevoked(revocation)) => {
4058 DeviceJoinStatus::JoinerClosed {
4059 terminal: JoinerJoinTerminal::WriteRevoked(revocation.clone()),
4060 }
4061 }
4062 DeviceJoinRoleProgress::Owner(OwnerJoinProgress::CleanupReceiptCreateIntent {
4063 cancellation,
4064 receipt,
4065 ..
4066 }) => DeviceJoinStatus::CleanupReceiptCreatePending {
4067 cancellation: cancellation.clone(),
4068 receipt: receipt.clone(),
4069 },
4070 DeviceJoinRoleProgress::Owner(OwnerJoinProgress::CleanupReceipt(receipt)) => {
4071 DeviceJoinStatus::AwaitingCleanupActivation {
4072 receipt: receipt.clone(),
4073 }
4074 }
4075 DeviceJoinRoleProgress::Owner(OwnerJoinProgress::CleanupActivated(activation))
4076 | DeviceJoinRoleProgress::Owner(OwnerJoinProgress::CancelledComplete(activation))
4077 | DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::CleanupActivated(activation))
4078 | DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::CancelledComplete(activation)) => {
4079 DeviceJoinStatus::CleanupActivated {
4080 activation: activation.clone(),
4081 }
4082 }
4083 _ => DeviceJoinStatus::OperationInProgress {
4084 attempt_id: record.attempt_id,
4085 },
4086 }
4087}
4088
4089pub async fn load_store_device_join_status(
4090 db: &Database,
4091 attempt_id: DeviceJoinAttemptId,
4092 role: DeviceJoinRole,
4093) -> Result<Option<DeviceJoinStatus>, DeviceJoinError> {
4094 load_store_journal(db, attempt_id, role)
4095 .await
4096 .map(|record| record.as_ref().map(device_join_status))
4097}
4098
4099pub fn load_pending_device_join_status(
4100 pending: &DeviceJoinJournalDatabase,
4101 attempt_id: DeviceJoinAttemptId,
4102) -> Result<Option<DeviceJoinStatus>, DeviceJoinError> {
4103 pending
4104 .load(attempt_id, DeviceJoinRole::Joiner)
4105 .map(|record| record.as_ref().map(device_join_status))
4106}
4107
4108pub async fn observe_device_join_abandonment(
4109 pending: &DeviceJoinJournalDatabase,
4110 storage: &dyn SyncStorage,
4111 root: &StoreRootRef,
4112 abandonment: DeviceJoinAbandonment,
4113) -> Result<DeviceJoinAbandonment, DeviceJoinError> {
4114 let current = pending
4115 .load(abandonment.abandonment.attempt_id, DeviceJoinRole::Joiner)?
4116 .ok_or(DeviceJoinError::JournalConflict)?;
4117 if let DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::Abandoned(existing)) =
4118 &*current.progress
4119 {
4120 if existing == &abandonment {
4121 return Ok(existing.clone());
4122 }
4123 return Err(DeviceJoinError::JournalConflict);
4124 }
4125 let context = crate::sync::storage::ProtocolObjectContext::store(
4126 root.store_root_hash,
4127 crate::sync::storage::ProtocolObjectDomain::DeviceJoinAbandonment,
4128 );
4129 let prefix = crate::sync::store_commit::device_join_abandonment_semantic_prefix(
4130 abandonment.abandonment.attempt_id,
4131 );
4132 let bytes = storage
4133 .read_protocol_object(&context, &abandonment.abandonment.object, &prefix)
4134 .await?;
4135 let object: DeviceJoinAbandonmentObject = serde_json::from_slice(&bytes)?;
4136 let owner = crate::sync::store_objects::load_registration_ref(
4137 storage,
4138 root,
4139 &object.owner_registration,
4140 )
4141 .await?
4142 .value;
4143 abandonment.abandonment.verify(&object, &owner)?;
4144 let (activation, author) = crate::sync::store_pull::load_commit_with_author(
4145 storage,
4146 root,
4147 &abandonment.abandonment_activation,
4148 )
4149 .await?;
4150 if author != owner
4151 || activation
4152 .device_join_abandonments()
4153 .binary_search(&abandonment.abandonment)
4154 .is_err()
4155 {
4156 return Err(DeviceJoinError::AttemptMismatch);
4157 }
4158 match &*current.progress {
4159 DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::AccessRequested(_))
4160 | DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::ApprovalReceived(_)) => {}
4161 _ => return Err(DeviceJoinError::JournalConflict),
4162 }
4163 pending.advance(
4164 ¤t,
4165 DeviceJoinJournalRecord {
4166 attempt_id: abandonment.abandonment.attempt_id,
4167 progress: Box::new(DeviceJoinRoleProgress::Joiner(
4168 JoinerJoinProgress::Abandoned(abandonment.clone()),
4169 )),
4170 },
4171 )?;
4172 Ok(abandonment)
4173}
4174
4175#[allow(clippy::too_many_arguments)]
4176pub async fn finalize_device_join(
4177 db: &Database,
4178 storage: &dyn SyncStorage,
4179 coordination: Option<&dyn CoordinationStorage>,
4180 authorization: &DeviceJoinAuthorization,
4181 identity_signer: &UserKeypair,
4182 completion: DeviceProviderAdmissionCompletion,
4183) -> Result<DeviceJoinActivation, DeviceJoinError> {
4184 require_authorization_policy(db, storage, authorization).await?;
4185 let attempt_ref = completion.readiness.proof.attempt.clone();
4186 let attempt_id = attempt_ref.attempt_id;
4187 let current = load_store_journal(db, attempt_id, DeviceJoinRole::Owner)
4188 .await?
4189 .ok_or(DeviceJoinError::JournalConflict)?;
4190 if let DeviceJoinRoleProgress::Owner(OwnerJoinProgress::ActivationPrepared(existing)) =
4191 &*current.progress
4192 {
4193 return Ok(existing.clone());
4194 }
4195 let provisional = match &*current.progress {
4196 DeviceJoinRoleProgress::Owner(OwnerJoinProgress::AttemptActivated(bootstrap)) => {
4197 bootstrap.clone()
4198 }
4199 _ => return Err(DeviceJoinError::JournalConflict),
4200 };
4201 let offer = &provisional.request.approval.request.offer;
4202 let owner = db
4203 .activated_store_device_registration(offer.owner_registration.clone())
4204 .await
4205 .map_err(database_error)?;
4206 let owner_signer = owner.device_signer(identity_signer)?;
4207 let attempt = crate::sync::store_objects::load_device_join_attempt_ref(
4208 storage,
4209 &offer.store_root,
4210 &attempt_ref,
4211 &owner,
4212 )
4213 .await?
4214 .value;
4215 let registration = crate::sync::store_objects::load_registration_ref(
4216 storage,
4217 &offer.store_root,
4218 &completion.readiness.proof.registration,
4219 )
4220 .await?
4221 .value;
4222 let ack = crate::sync::store_objects::load_store_ack_ref(
4223 storage,
4224 &offer.store_root,
4225 &completion.readiness.proof.initial_ack,
4226 ®istration,
4227 )
4228 .await?
4229 .value;
4230 completion.readiness.proof.verify(
4231 &attempt_ref,
4232 &attempt,
4233 ®istration,
4234 &completion.readiness.proof.initial_ack,
4235 &ack,
4236 )?;
4237 match (&attempt.provider_approval.admission, &completion.admission) {
4238 (
4239 DeviceProviderAdmissionChallenge::SamePrincipal,
4240 DeviceProviderAdmission::SamePrincipal,
4241 ) => {}
4242 (
4243 DeviceProviderAdmissionChallenge::CrossPrincipal(challenge),
4244 DeviceProviderAdmission::CrossPrincipal(receipt),
4245 ) => {
4246 let administrator = db
4247 .activated_store_device_registration(offer.provider_admin.administrator.clone())
4248 .await
4249 .map_err(database_error)?;
4250 let response_slot = match &attempt.provider_response {
4251 DeviceProviderResponseReservation::CrossPrincipal { response_slot } => {
4252 response_slot.clone()
4253 }
4254 DeviceProviderResponseReservation::SamePrincipal => {
4255 return Err(DeviceJoinError::AttemptMismatch);
4256 }
4257 };
4258 let context = crate::sync::provider::CrossPrincipalResponseContext {
4259 challenge: cross_challenge_context(&attempt.provider_approval.request),
4260 expected_registration_hash: attempt.expected_registration.registration_hash(),
4261 response_slot,
4262 };
4263 receipt
4264 .verify(
4265 &context,
4266 &offer.provider,
4267 &administrator.device_signing_pubkey,
4268 &offer.member_pubkey,
4269 )
4270 .map_err(provider_error)?;
4271 if &receipt.transcript.challenge != challenge {
4272 return Err(DeviceJoinError::AttemptMismatch);
4273 }
4274 }
4275 _ => return Err(DeviceJoinError::AttemptMismatch),
4276 }
4277 let outcome = crate::sync::store_commit::DeviceJoinOutcome::signed(
4278 attempt_ref.clone(),
4279 crate::sync::store_commit::DeviceJoinOutcomeBody::Activated {
4280 readiness: completion.readiness.proof.clone(),
4281 },
4282 offer.owner_registration.clone(),
4283 offer.owner_grant.clone(),
4284 &owner,
4285 &owner_signer,
4286 )?;
4287 let context = crate::sync::storage::ProtocolObjectContext::store(
4288 offer.store_root.store_root_hash,
4289 crate::sync::storage::ProtocolObjectDomain::DeviceJoinOutcome,
4290 );
4291 let prefix = crate::sync::store_commit::device_join_outcome_semantic_prefix(attempt_id);
4292 let prepared = storage.prepare_protocol_object(
4293 &context,
4294 attempt.outcome_slot.clone(),
4295 &prefix,
4296 outcome.to_bytes(),
4297 )?;
4298 storage.create_protocol_object(&prepared).await?;
4299 let opened = storage
4300 .read_protocol_object(&context, prepared.reference(), &prefix)
4301 .await?;
4302 if opened != outcome.to_bytes() {
4303 return Err(DeviceJoinError::AttemptMismatch);
4304 }
4305 let outcome_ref = DeviceJoinOutcomeRef::Activated {
4306 attempt: attempt_ref,
4307 outcome_hash: outcome.outcome_hash(),
4308 object: prepared.reference().clone(),
4309 };
4310 let activated_registration = crate::sync::store_outbound::DeviceJoinRegistrationActivation {
4311 reference: crate::sync::store_commit::ActivatedStoreDeviceRegistrationRef {
4312 registration: completion.readiness.proof.registration.clone(),
4313 authority: crate::sync::store_commit::StoreDeviceRegistrationActivationRef::Join {
4314 attempt_id,
4315 outcome: outcome_ref.clone(),
4316 },
4317 },
4318 registration: attempt.expected_registration.clone(),
4319 authority: crate::sync::store_commit::StoreDeviceRegistrationActivation::Join {
4320 attempt_id,
4321 outcome: outcome_ref.clone(),
4322 },
4323 };
4324 let local_device_id = db
4325 .get_protocol_state(crate::database::LOCAL_DEVICE_ID_STATE_KEY)
4326 .await
4327 .map_err(database_error)?
4328 .ok_or(DeviceJoinError::ActiveDeviceRequired)?;
4329 let plan = crate::sync::store_outbound::prepare_device_join_commit(
4330 db,
4331 storage,
4332 coordination,
4333 &local_device_id,
4334 identity_signer,
4335 authorization.merge_chain(),
4336 )
4337 .await?;
4338 let activation_ref = crate::sync::store_outbound::activate_device_join_commit(
4339 db,
4340 storage,
4341 coordination,
4342 plan,
4343 crate::sync::store_outbound::DeviceJoinStoreBatch::Outcome {
4344 outcome: outcome_ref.clone(),
4345 registration: Some(activated_registration),
4346 },
4347 )
4348 .await?;
4349 let activation = DeviceJoinActivation {
4350 outcome: outcome_ref,
4351 outcome_activation: activation_ref,
4352 };
4353 let provider_ready = ProviderReadyDeviceBootstrap {
4354 bootstrap: Box::new(provisional),
4355 challenge_publication: match &attempt.provider_approval.admission {
4356 DeviceProviderAdmissionChallenge::SamePrincipal => {
4357 DeviceProviderChallengePublication::SamePrincipal
4358 }
4359 DeviceProviderAdmissionChallenge::CrossPrincipal(challenge) => {
4360 DeviceProviderChallengePublication::CrossPrincipal {
4361 challenge: challenge.clone(),
4362 }
4363 }
4364 },
4365 };
4366 let ready_record = DeviceJoinJournalRecord {
4367 attempt_id,
4368 progress: Box::new(DeviceJoinRoleProgress::Owner(
4369 OwnerJoinProgress::ProviderReady(provider_ready),
4370 )),
4371 };
4372 advance_store_journal(db, ¤t, ready_record.clone()).await?;
4373 let completion_record = DeviceJoinJournalRecord {
4374 attempt_id,
4375 progress: Box::new(DeviceJoinRoleProgress::Owner(
4376 OwnerJoinProgress::AdmissionCompleted(completion),
4377 )),
4378 };
4379 advance_store_journal(db, &ready_record, completion_record.clone()).await?;
4380 advance_store_journal(
4381 db,
4382 &completion_record,
4383 DeviceJoinJournalRecord {
4384 attempt_id,
4385 progress: Box::new(DeviceJoinRoleProgress::Owner(
4386 OwnerJoinProgress::ActivationPrepared(activation.clone()),
4387 )),
4388 },
4389 )
4390 .await?;
4391 Ok(activation)
4392}
4393
4394pub async fn materialize_device_join_activation(
4395 db: &Database,
4396 storage: &dyn SyncStorage,
4397 activation: DeviceJoinActivation,
4398) -> Result<JoinedStore, DeviceJoinError> {
4399 if !matches!(activation.outcome, DeviceJoinOutcomeRef::Activated { .. }) {
4400 return Err(DeviceJoinError::AttemptMismatch);
4401 }
4402 let root = db
4403 .local_store_root_ref()
4404 .await
4405 .map_err(database_error)?
4406 .ok_or(DeviceJoinError::ActiveDeviceRequired)?;
4407 let attempt_ref = activation.outcome.attempt().clone();
4408 let attempt_context = crate::sync::storage::ProtocolObjectContext::store(
4409 root.store_root_hash,
4410 crate::sync::storage::ProtocolObjectDomain::DeviceJoinAttempt,
4411 );
4412 let attempt_bytes = storage
4413 .read_protocol_object(
4414 &attempt_context,
4415 &attempt_ref.object,
4416 &crate::sync::store_commit::device_join_attempt_semantic_prefix(attempt_ref.attempt_id),
4417 )
4418 .await?;
4419 let unverified_attempt: DeviceJoinAttempt = serde_json::from_slice(&attempt_bytes)?;
4420 let owner = db
4421 .activated_store_device_registration(unverified_attempt.owner_registration.clone())
4422 .await
4423 .map_err(database_error)?;
4424 let attempt = crate::sync::store_objects::load_device_join_attempt_ref(
4425 storage,
4426 &root,
4427 &attempt_ref,
4428 &owner,
4429 )
4430 .await?
4431 .value;
4432 let authorization = Box::pin(crate::sync::store_pull::load_device_join_authorization(
4433 storage,
4434 &root,
4435 &attempt.membership,
4436 ))
4437 .await?;
4438 Box::pin(crate::sync::store_pull::materialize_device_join_activation(
4439 db,
4440 storage,
4441 &root,
4442 &activation.outcome_activation,
4443 &activation.outcome,
4444 &authorization,
4445 ))
4446 .await?;
4447 let outcome = crate::sync::store_objects::load_device_join_outcome_ref(
4448 storage,
4449 &root,
4450 &activation.outcome,
4451 &owner,
4452 )
4453 .await?
4454 .value;
4455 let crate::sync::store_commit::DeviceJoinOutcomeBody::Activated { readiness } = outcome.body
4456 else {
4457 return Err(DeviceJoinError::AttemptMismatch);
4458 };
4459 let local = db
4460 .latest_local_store_device_registration()
4461 .await
4462 .map_err(database_error)?
4463 .ok_or(DeviceJoinError::ActiveDeviceRequired)?;
4464 if !local.is_activated()
4465 || local.registration_hash != readiness.registration.registration_hash
4466 || local.device_id != readiness.registration.device_id
4467 || attempt.expected_registration.to_bytes() != local.registration_bytes
4468 {
4469 return Err(DeviceJoinError::ActivationNotMaterialized);
4470 }
4471 let joined = JoinedStore {
4472 store_root: root,
4473 registration: readiness.registration.clone(),
4474 activation,
4475 };
4476 Ok(joined)
4477}
4478
4479pub async fn complete_device_join(
4480 db: &Database,
4481 pending: &DeviceJoinJournalDatabase,
4482 storage: &dyn SyncStorage,
4483 activation: DeviceJoinActivation,
4484) -> Result<JoinedStore, DeviceJoinError> {
4485 let attempt_id = activation.outcome.attempt().attempt_id;
4486 let joined = materialize_device_join_activation(db, storage, activation).await?;
4487 if let Some(record) = load_store_journal(db, attempt_id, DeviceJoinRole::Joiner).await? {
4488 return match &*record.progress {
4489 DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::Activated(existing))
4490 if existing == &joined =>
4491 {
4492 Ok(joined)
4493 }
4494 _ => Err(DeviceJoinError::JournalConflict),
4495 };
4496 }
4497 let current = pending
4498 .load(attempt_id, DeviceJoinRole::Joiner)?
4499 .ok_or(DeviceJoinError::JournalConflict)?;
4500 let DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::Ready(current_readiness)) =
4501 &*current.progress
4502 else {
4503 return Err(DeviceJoinError::JournalConflict);
4504 };
4505 if current_readiness.proof.registration != joined.registration {
4506 return Err(DeviceJoinError::JournalConflict);
4507 }
4508 let activated_record = DeviceJoinJournalRecord {
4509 attempt_id,
4510 progress: Box::new(DeviceJoinRoleProgress::Joiner(
4511 JoinerJoinProgress::Activated(joined.clone()),
4512 )),
4513 };
4514 let store_key = store_journal_key(attempt_id, DeviceJoinRole::Joiner.as_str());
4515 let store_payload = serde_json::to_string(&activated_record)?;
4516 let pending_path = pending.path().to_string_lossy().into_owned();
4517 let pending_attempt = attempt_key(attempt_id);
4518 let expected_pending = serde_json::to_string(¤t)?;
4519 db.call(move |connection| {
4520 connection
4521 .execute("ATTACH DATABASE ?1 AS pending_join_source", [&pending_path])
4522 .map_err(crate::database::DbError::from)?;
4523 let tx = connection
4524 .unchecked_transaction()
4525 .map_err(crate::database::DbError::from)?;
4526 let actual: String = tx
4527 .query_row(
4528 "SELECT payload FROM pending_join_source.device_join_journals
4529 WHERE attempt_id = ?1 AND role = 'joiner'",
4530 [&pending_attempt],
4531 |row| row.get(0),
4532 )
4533 .map_err(crate::database::DbError::from)?;
4534 if actual != expected_pending {
4535 return Err(crate::database::DbError::Message(
4536 "pending join journal changed before activation".to_string(),
4537 ));
4538 }
4539 tx.execute(
4540 "INSERT INTO protocol_state (key, value) VALUES (?1, ?2)
4541 ON CONFLICT(key) DO UPDATE SET value = excluded.value WHERE value = excluded.value",
4542 (&store_key, &store_payload),
4543 )
4544 .map_err(crate::database::DbError::from)?;
4545 tx.execute(
4546 "DELETE FROM pending_join_source.device_join_journals
4547 WHERE attempt_id = ?1 AND role = 'joiner' AND payload = ?2",
4548 (&pending_attempt, &expected_pending),
4549 )
4550 .map_err(crate::database::DbError::from)?;
4551 tx.commit().map_err(crate::database::DbError::from)?;
4552 connection
4553 .execute_batch("DETACH DATABASE pending_join_source")
4554 .map_err(crate::database::DbError::from)
4555 })
4556 .await
4557 .map_err(database_error)?;
4558 Ok(joined)
4559}
4560
4561#[allow(clippy::too_many_arguments)]
4562pub async fn authorize_device_provider_access(
4563 db: &Database,
4564 storage: &dyn SyncStorage,
4565 coordination: Option<&dyn CoordinationStorage>,
4566 administrator_exact: Option<&dyn ExactSlotStorage>,
4567 access_administrator: Option<&dyn DeviceProviderAccessAdministrator>,
4568 authorization: &DeviceJoinAuthorization,
4569 identity_signer: &UserKeypair,
4570 request: DeviceProviderAccessRequest,
4571) -> Result<DeviceProviderAdmissionApproval, DeviceJoinError> {
4572 require_authorization_policy(db, storage, authorization).await?;
4573 let owner = db
4574 .activated_store_device_registration(request.offer.owner_registration.clone())
4575 .await
4576 .map_err(database_error)?;
4577 request.verify(&owner)?;
4578 let provider_admin =
4579 authorization.resolved_provider_admin(&request.offer.provider_admin.grant_id)?;
4580 if provider_admin != *request.offer.provider_admin {
4581 return Err(DeviceJoinError::OfferMismatch);
4582 }
4583 let administrator = db
4584 .activated_store_device_registration(provider_admin.administrator.clone())
4585 .await
4586 .map_err(database_error)?;
4587 let local_device_id = db
4588 .get_protocol_state(crate::database::LOCAL_DEVICE_ID_STATE_KEY)
4589 .await
4590 .map_err(database_error)?
4591 .ok_or(DeviceJoinError::ActiveDeviceRequired)?;
4592 if administrator.device_id.to_string() != local_device_id {
4593 return Err(DeviceJoinError::ProviderAdministratorRequired);
4594 }
4595 let administrator_signer = administrator.device_signer(identity_signer)?;
4596 let initial = DeviceJoinJournalRecord {
4597 attempt_id: request.offer.attempt_id,
4598 progress: Box::new(DeviceJoinRoleProgress::ProviderAdministrator(
4599 ProviderAdminJoinProgress::AccessRequested(request.clone()),
4600 )),
4601 };
4602 let durable = begin_store_journal(db, initial.clone()).await?;
4603 let (grant, prepared, prepared_progress) = match &*durable.progress {
4604 DeviceJoinRoleProgress::ProviderAdministrator(
4605 ProviderAdminJoinProgress::ApprovalPrepared(approval),
4606 ) => return Ok(approval.clone()),
4607 DeviceJoinRoleProgress::ProviderAdministrator(
4608 ProviderAdminJoinProgress::AccessGrantPrepared {
4609 request: durable_request,
4610 grant,
4611 prepared,
4612 },
4613 ) if durable_request == &request => (
4614 grant.clone(),
4615 crate::sync::storage::PreparedExactObject::new(
4616 prepared.object.clone(),
4617 prepared.stored_bytes.clone(),
4618 )?,
4619 durable.clone(),
4620 ),
4621 DeviceJoinRoleProgress::ProviderAdministrator(
4622 ProviderAdminJoinProgress::AccessRequested(durable_request),
4623 ) if durable_request == &request => {
4624 let locator = if provider_admin.provider == request.peer_provider {
4625 provider_admin.access.clone()
4626 } else {
4627 let administrator =
4628 access_administrator.ok_or(DeviceJoinError::ProviderAdministratorRequired)?;
4629 administrator
4630 .grant_member_access(
4631 &request.offer.member_pubkey,
4632 authorization.current_member_provider_email(&request.offer.member_pubkey),
4633 &request.peer_provider,
4634 )
4635 .await?
4636 };
4637 let grant_id = ProviderAccessGrantId::from_random_bytes(
4638 *ObjectHash::digest(db.new_write_id().as_str().as_bytes()).as_bytes(),
4639 );
4640 let grant = StoreMemberProviderAccessGrant::signed(
4641 grant_id,
4642 request.offer.member_pubkey.clone(),
4643 request.peer_provider.clone(),
4644 locator,
4645 provider_admin.grant_id.clone(),
4646 provider_admin.administrator.clone(),
4647 &request.offer.provider,
4648 &administrator,
4649 &administrator_signer,
4650 )
4651 .map_err(provider_error)?;
4652 let context = crate::sync::storage::ProtocolObjectContext::store(
4653 request.offer.store_root.store_root_hash,
4654 crate::sync::storage::ProtocolObjectDomain::ProviderAccessGrant,
4655 );
4656 let prefix =
4657 crate::sync::store_commit::provider_access_grant_semantic_prefix(&grant.grant_id);
4658 let slot = storage
4659 .allocate_protocol_slot(&context, &prefix, ".json")
4660 .await?;
4661 let prepared =
4662 storage.prepare_protocol_object(&context, slot, &prefix, grant.to_bytes())?;
4663 let prepared_progress = DeviceJoinJournalRecord {
4664 attempt_id: request.offer.attempt_id,
4665 progress: Box::new(DeviceJoinRoleProgress::ProviderAdministrator(
4666 ProviderAdminJoinProgress::AccessGrantPrepared {
4667 request: request.clone(),
4668 grant: grant.clone(),
4669 prepared: PreparedDeviceJoinObject::from_prepared(&prepared),
4670 },
4671 )),
4672 };
4673 advance_store_journal(db, &initial, prepared_progress.clone()).await?;
4674 (grant, prepared, prepared_progress)
4675 }
4676 _ => return Err(DeviceJoinError::JournalConflict),
4677 };
4678 let context = crate::sync::storage::ProtocolObjectContext::store(
4679 request.offer.store_root.store_root_hash,
4680 crate::sync::storage::ProtocolObjectDomain::ProviderAccessGrant,
4681 );
4682 let prefix = crate::sync::store_commit::provider_access_grant_semantic_prefix(&grant.grant_id);
4683 storage.create_protocol_object(&prepared).await?;
4684 let opened = storage
4685 .read_protocol_object(&context, prepared.reference(), &prefix)
4686 .await?;
4687 if opened != grant.to_bytes() {
4688 return Err(DeviceJoinError::Provider(
4689 "provider access grant exact readback differs from its signed bytes".to_string(),
4690 ));
4691 }
4692 let grant_ref =
4693 StoreMemberProviderAccessGrantRef::from_grant(&grant, prepared.reference().clone());
4694 let plan = crate::sync::store_outbound::prepare_device_join_commit(
4695 db,
4696 storage,
4697 coordination,
4698 &local_device_id,
4699 identity_signer,
4700 authorization.merge_chain(),
4701 )
4702 .await?;
4703 let activation = crate::sync::store_outbound::activate_device_join_commit(
4704 db,
4705 storage,
4706 coordination,
4707 plan,
4708 crate::sync::store_outbound::DeviceJoinStoreBatch::ProviderAccessGrant(grant_ref.clone()),
4709 )
4710 .await?;
4711 let admission = if provider_admin.provider == request.peer_provider {
4712 DeviceProviderAdmissionChallenge::SamePrincipal
4713 } else {
4714 let exact = administrator_exact.ok_or(DeviceJoinError::ProviderAdministratorRequired)?;
4715 let challenge_context = crate::sync::provider::CrossPrincipalChallengeContext {
4716 root: request.offer.store_root.clone(),
4717 attempt_id: request.offer.attempt_id,
4718 access_request_hash: request.request_hash(),
4719 provider_admin_grant: provider_admin.grant_id.clone(),
4720 owner_registration: request.offer.owner_registration.clone(),
4721 member_pubkey: request.offer.member_pubkey.clone(),
4722 administrator_binding: provider_admin.provider.clone(),
4723 peer_binding: request.peer_provider.clone(),
4724 };
4725 let probe_id = crate::sync::provider::ProviderProbeId::from_bytes(
4726 *ObjectHash::digest(db.new_write_id().as_str().as_bytes()).as_bytes(),
4727 );
4728 DeviceProviderAdmissionChallenge::CrossPrincipal(
4729 crate::sync::provider::prepare_cross_principal_challenge(
4730 exact,
4731 db,
4732 probe_id,
4733 &request.offer.provider,
4734 &challenge_context,
4735 &administrator_signer,
4736 )
4737 .await
4738 .map_err(provider_error)?,
4739 )
4740 };
4741 let approval = DeviceProviderAdmissionApproval::signed(
4742 request,
4743 ActivatedStoreMemberProviderAccessGrant {
4744 grant,
4745 grant_ref,
4746 activation,
4747 },
4748 admission,
4749 &administrator,
4750 &administrator_signer,
4751 )?;
4752 advance_store_journal(
4753 db,
4754 &prepared_progress,
4755 DeviceJoinJournalRecord {
4756 attempt_id: approval.request.offer.attempt_id,
4757 progress: Box::new(DeviceJoinRoleProgress::ProviderAdministrator(
4758 ProviderAdminJoinProgress::ApprovalPrepared(approval.clone()),
4759 )),
4760 },
4761 )
4762 .await?;
4763 Ok(approval)
4764}
4765
4766async fn begin_store_journal(
4767 db: &Database,
4768 record: DeviceJoinJournalRecord,
4769) -> Result<DeviceJoinJournalRecord, DeviceJoinError> {
4770 validate_initial_progress(&record.progress)?;
4771 let key = store_journal_key(record.attempt_id, record.progress.role_name());
4772 let value = serde_json::to_string(&record)?;
4773 db.call(move |connection| {
4774 connection
4775 .execute(
4776 "INSERT OR IGNORE INTO protocol_state (key, value) VALUES (?1, ?2)",
4777 (&key, &value),
4778 )
4779 .map_err(crate::database::DbError::from)?;
4780 let actual = connection
4781 .query_row(
4782 "SELECT value FROM protocol_state WHERE key = ?1",
4783 [&key],
4784 |row| row.get::<_, String>(0),
4785 )
4786 .map_err(crate::database::DbError::from)?;
4787 serde_json::from_str(&actual)
4788 .map_err(|error| crate::database::DbError::Message(error.to_string()))
4789 })
4790 .await
4791 .map_err(database_error)
4792}
4793
4794async fn load_store_journal(
4795 db: &Database,
4796 attempt_id: DeviceJoinAttemptId,
4797 role: DeviceJoinRole,
4798) -> Result<Option<DeviceJoinJournalRecord>, DeviceJoinError> {
4799 let key = store_journal_key(attempt_id, role.as_str());
4800 let value = db.get_protocol_state(&key).await.map_err(database_error)?;
4801 value
4802 .map(|value| serde_json::from_str(&value).map_err(DeviceJoinError::from))
4803 .transpose()
4804}
4805
4806async fn advance_store_journal(
4807 db: &Database,
4808 previous: &DeviceJoinJournalRecord,
4809 next: DeviceJoinJournalRecord,
4810) -> Result<(), DeviceJoinError> {
4811 if previous.attempt_id != next.attempt_id {
4812 return Err(DeviceJoinError::JournalConflict);
4813 }
4814 previous.progress.validate_transition(&next.progress)?;
4815 let key = store_journal_key(previous.attempt_id, previous.progress.role_name());
4816 let previous = serde_json::to_string(previous)?;
4817 let next = serde_json::to_string(&next)?;
4818 let changed = db
4819 .call(move |connection| {
4820 connection
4821 .execute(
4822 "UPDATE protocol_state SET value = ?1 WHERE key = ?2 AND value = ?3",
4823 (&next, &key, &previous),
4824 )
4825 .map_err(crate::database::DbError::from)
4826 })
4827 .await
4828 .map_err(database_error)?;
4829 if changed == 1 {
4830 Ok(())
4831 } else {
4832 Err(DeviceJoinError::JournalConflict)
4833 }
4834}
4835
4836fn store_journal_key(attempt_id: DeviceJoinAttemptId, role: &str) -> String {
4837 format!("device_join/{}/{role}", attempt_key(attempt_id))
4838}
4839
4840fn database_error(error: crate::database::DbError) -> DeviceJoinError {
4841 DeviceJoinError::Store(error.into_message())
4842}
4843
4844fn provider_error(error: impl std::fmt::Display) -> DeviceJoinError {
4845 DeviceJoinError::Provider(error.to_string())
4846}
4847
4848pub fn validate_member_for_join(
4849 member_pubkey: &str,
4850 members: &[(String, MemberRole)],
4851) -> Result<(), DeviceJoinError> {
4852 if members
4853 .iter()
4854 .any(|(pubkey, role)| pubkey == member_pubkey && role.can_write())
4855 {
4856 Ok(())
4857 } else {
4858 Err(DeviceJoinError::MemberNotEligible)
4859 }
4860}
4861
4862pub fn canonical_cleanup_slots(
4863 attempt: &DeviceJoinAttempt,
4864) -> Result<Vec<ObjectSlot>, DeviceJoinError> {
4865 let mut slots = vec![
4866 attempt.registration_slot.clone(),
4867 attempt
4868 .expected_registration
4869 .acknowledgements
4870 .first_slot()
4871 .clone(),
4872 ];
4873 match (
4874 &attempt.provider_approval.admission,
4875 &attempt.provider_response,
4876 ) {
4877 (
4878 DeviceProviderAdmissionChallenge::SamePrincipal,
4879 DeviceProviderResponseReservation::SamePrincipal,
4880 ) => {}
4881 (
4882 DeviceProviderAdmissionChallenge::CrossPrincipal(challenge),
4883 DeviceProviderResponseReservation::CrossPrincipal { response_slot },
4884 ) => {
4885 slots.push(challenge.administrator_object.slot.clone());
4886 slots.push(response_slot.clone());
4887 }
4888 _ => return Err(DeviceJoinError::AttemptMismatch),
4889 }
4890 slots.sort();
4891 require_distinct_slots(&slots)?;
4892 Ok(slots)
4893}
4894
4895fn require_cancelled_outcome(outcome: &DeviceJoinOutcomeRef) -> Result<(), DeviceJoinError> {
4896 if matches!(outcome, DeviceJoinOutcomeRef::Cancelled { .. }) {
4897 Ok(())
4898 } else {
4899 Err(DeviceJoinError::AttemptMismatch)
4900 }
4901}
4902
4903fn validate_terminals(
4904 cancellation: &DeviceJoinOutcomeRef,
4905 administrator: &ProviderAdminJoinTerminal,
4906 joiner: &JoinerJoinTerminal,
4907) -> Result<(), DeviceJoinError> {
4908 let administrator_cancellation = match administrator {
4909 ProviderAdminJoinTerminal::Completed(completion) => {
4910 if completion.readiness.proof.attempt != *cancellation.attempt() {
4911 return Err(DeviceJoinError::AttemptMismatch);
4912 }
4913 None
4914 }
4915 ProviderAdminJoinTerminal::Cancelled(closure) => Some(&closure.cancellation),
4916 ProviderAdminJoinTerminal::WriteRevoked(revocation) => Some(&revocation.cancellation),
4917 };
4918 let joiner_cancellation = match joiner {
4919 JoinerJoinTerminal::Ready(readiness) => {
4920 if readiness.proof.attempt != *cancellation.attempt() {
4921 return Err(DeviceJoinError::AttemptMismatch);
4922 }
4923 None
4924 }
4925 JoinerJoinTerminal::Cancelled(closure) => Some(&closure.cancellation),
4926 JoinerJoinTerminal::WriteRevoked(revocation) => Some(&revocation.cancellation),
4927 };
4928 if administrator_cancellation.is_some_and(|value| value != cancellation)
4929 || joiner_cancellation.is_some_and(|value| value != cancellation)
4930 {
4931 return Err(DeviceJoinError::AttemptMismatch);
4932 }
4933 Ok(())
4934}
4935
4936async fn verify_cleanup_terminals(
4937 db: &Database,
4938 administrator: &ProviderAdminJoinTerminal,
4939 joiner: &JoinerJoinTerminal,
4940) -> Result<(), DeviceJoinError> {
4941 match administrator {
4942 ProviderAdminJoinTerminal::Completed(_) => {}
4943 ProviderAdminJoinTerminal::Cancelled(closure) => {
4944 let registration = db
4945 .activated_store_device_registration(closure.administrator_registration.clone())
4946 .await
4947 .map_err(database_error)?;
4948 closure.verify(®istration)?;
4949 }
4950 ProviderAdminJoinTerminal::WriteRevoked(revocation) => {
4951 let registration = db
4952 .activated_store_device_registration(revocation.executor.clone())
4953 .await
4954 .map_err(database_error)?;
4955 revocation.verify(®istration)?;
4956 }
4957 }
4958 match joiner {
4959 JoinerJoinTerminal::Ready(_) => {}
4960 JoinerJoinTerminal::Cancelled(closure) => closure.verify()?,
4961 JoinerJoinTerminal::WriteRevoked(revocation) => {
4962 let registration = db
4963 .activated_store_device_registration(revocation.executor.clone())
4964 .await
4965 .map_err(database_error)?;
4966 revocation.verify(®istration)?;
4967 }
4968 }
4969 Ok(())
4970}
4971
4972async fn ensure_exact_slot_absent(
4973 storage: &dyn ExactSlotStorage,
4974 slot: &ObjectSlot,
4975) -> Result<(), DeviceJoinError> {
4976 match storage.read_at(slot).await {
4977 Err(crate::storage::cloud::CloudHomeError::NotFound(_)) => Ok(()),
4978 Ok(_) => {
4979 storage
4980 .delete_at(slot)
4981 .await
4982 .map_err(|error| DeviceJoinError::Provider(error.to_string()))?;
4983 match storage.read_at(slot).await {
4984 Err(crate::storage::cloud::CloudHomeError::NotFound(_)) => Ok(()),
4985 Ok(_) => Err(DeviceJoinError::CleanupMismatch),
4986 Err(error) => Err(DeviceJoinError::Provider(error.to_string())),
4987 }
4988 }
4989 Err(error) => Err(DeviceJoinError::Provider(error.to_string())),
4990 }
4991}
4992
4993async fn observe_exact_slot(
4994 storage: &dyn ExactSlotStorage,
4995 slot: &ObjectSlot,
4996) -> Result<SlotDisposition, DeviceJoinError> {
4997 match storage.read_at(slot).await {
4998 Err(crate::storage::cloud::CloudHomeError::NotFound(_)) => {
4999 Ok(SlotDisposition::NeverCreated)
5000 }
5001 Ok(bytes) => Ok(SlotDisposition::Created(ExactObjectRef::new(
5002 slot.clone(),
5003 bytes.len() as u64,
5004 ObjectHash::digest(&bytes),
5005 ))),
5006 Err(error) => Err(DeviceJoinError::Provider(error.to_string())),
5007 }
5008}
5009
5010fn withdrawal_matches_locator(
5011 withdrawal: &ProviderAccessWithdrawal,
5012 locator: &crate::sync::provider::ProviderAccessLocator,
5013) -> bool {
5014 match (withdrawal, locator) {
5015 (
5016 ProviderAccessWithdrawal::Direct {
5017 locator: withdrawn,
5018 verified_absent: true,
5019 },
5020 expected,
5021 ) => withdrawn == expected,
5022 (
5023 ProviderAccessWithdrawal::S3CredentialRotation {
5024 retired_generation,
5025 active_generation,
5026 retired_credential_verified_rejected: true,
5027 },
5028 crate::sync::provider::ProviderAccessLocator::S3SharedCredentialGeneration {
5029 generation,
5030 ..
5031 },
5032 ) => retired_generation == generation && *active_generation == generation.saturating_add(1),
5033 _ => false,
5034 }
5035}
5036
5037fn owner_adjacent(previous: &OwnerJoinProgress, next: &OwnerJoinProgress) -> bool {
5038 matches!(
5039 (previous, next),
5040 (
5041 OwnerJoinProgress::Offered(_),
5042 OwnerJoinProgress::RegistrationRequested(_)
5043 ) | (
5044 OwnerJoinProgress::Offered(_),
5045 OwnerJoinProgress::AbandonmentCreateIntent { .. }
5046 ) | (
5047 OwnerJoinProgress::RegistrationRequested(_),
5048 OwnerJoinProgress::AttemptActivated(_)
5049 ) | (
5050 OwnerJoinProgress::RegistrationRequested(_),
5051 OwnerJoinProgress::AbandonmentCreateIntent { .. }
5052 ) | (
5053 OwnerJoinProgress::AbandonmentCreateIntent { .. },
5054 OwnerJoinProgress::Abandoned(_)
5055 ) | (
5056 OwnerJoinProgress::AttemptActivated(_),
5057 OwnerJoinProgress::ProviderReady(_)
5058 ) | (
5059 OwnerJoinProgress::AttemptActivated(_),
5060 OwnerJoinProgress::CancellationCreateIntent { .. }
5061 ) | (
5062 OwnerJoinProgress::ProviderReady(_),
5063 OwnerJoinProgress::AdmissionCompleted(_)
5064 ) | (
5065 OwnerJoinProgress::ProviderReady(_),
5066 OwnerJoinProgress::CancellationCreateIntent { .. }
5067 ) | (
5068 OwnerJoinProgress::AdmissionCompleted(_),
5069 OwnerJoinProgress::ActivationPrepared(_)
5070 ) | (
5071 OwnerJoinProgress::AdmissionCompleted(_),
5072 OwnerJoinProgress::CancellationCreateIntent { .. }
5073 ) | (
5074 OwnerJoinProgress::CancellationCreateIntent { .. },
5075 OwnerJoinProgress::Cancelled(_)
5076 ) | (
5077 OwnerJoinProgress::ActivationPrepared(_),
5078 OwnerJoinProgress::Activated(_)
5079 ) | (
5080 OwnerJoinProgress::Cancelled(_),
5081 OwnerJoinProgress::CleanupReceiptCreateIntent { .. }
5082 ) | (
5083 OwnerJoinProgress::CleanupReceiptCreateIntent { .. },
5084 OwnerJoinProgress::CleanupReceipt(_)
5085 ) | (
5086 OwnerJoinProgress::CleanupReceipt(_),
5087 OwnerJoinProgress::CleanupActivated(_)
5088 ) | (
5089 OwnerJoinProgress::CleanupActivated(_),
5090 OwnerJoinProgress::CancelledComplete(_)
5091 )
5092 )
5093}
5094
5095fn provider_admin_adjacent(
5096 previous: &ProviderAdminJoinProgress,
5097 next: &ProviderAdminJoinProgress,
5098) -> bool {
5099 matches!(
5100 (previous, next),
5101 (
5102 ProviderAdminJoinProgress::AccessRequested(_),
5103 ProviderAdminJoinProgress::AccessGrantPrepared { .. }
5104 ) | (
5105 ProviderAdminJoinProgress::AccessGrantPrepared { .. },
5106 ProviderAdminJoinProgress::ApprovalPrepared(_)
5107 ) | (
5108 ProviderAdminJoinProgress::ApprovalPrepared(_),
5109 ProviderAdminJoinProgress::AttemptObserved(_)
5110 ) | (
5111 ProviderAdminJoinProgress::ApprovalPrepared(_),
5112 ProviderAdminJoinProgress::CleanupIntent { .. }
5113 ) | (
5114 ProviderAdminJoinProgress::AttemptObserved(_),
5115 ProviderAdminJoinProgress::ChallengeCreateIntent(_)
5116 ) | (
5117 ProviderAdminJoinProgress::AttemptObserved(_),
5118 ProviderAdminJoinProgress::CleanupIntent { .. }
5119 ) | (
5120 ProviderAdminJoinProgress::ChallengeCreateIntent(_),
5121 ProviderAdminJoinProgress::ProviderReady(_)
5122 ) | (
5123 ProviderAdminJoinProgress::ChallengeCreateIntent(_),
5124 ProviderAdminJoinProgress::CleanupIntent { .. }
5125 ) | (
5126 ProviderAdminJoinProgress::ProviderReady(_),
5127 ProviderAdminJoinProgress::ResponseObserved(_)
5128 ) | (
5129 ProviderAdminJoinProgress::ProviderReady(_),
5130 ProviderAdminJoinProgress::CleanupIntent { .. }
5131 ) | (
5132 ProviderAdminJoinProgress::ResponseObserved(_),
5133 ProviderAdminJoinProgress::Completed(_)
5134 ) | (
5135 ProviderAdminJoinProgress::ResponseObserved(_),
5136 ProviderAdminJoinProgress::CleanupIntent { .. }
5137 ) | (
5138 ProviderAdminJoinProgress::ApprovalPrepared(_),
5139 ProviderAdminJoinProgress::WriteRevoked(_)
5140 ) | (
5141 ProviderAdminJoinProgress::AttemptObserved(_),
5142 ProviderAdminJoinProgress::WriteRevoked(_)
5143 ) | (
5144 ProviderAdminJoinProgress::ChallengeCreateIntent(_),
5145 ProviderAdminJoinProgress::WriteRevoked(_)
5146 ) | (
5147 ProviderAdminJoinProgress::ProviderReady(_),
5148 ProviderAdminJoinProgress::WriteRevoked(_)
5149 ) | (
5150 ProviderAdminJoinProgress::ResponseObserved(_),
5151 ProviderAdminJoinProgress::WriteRevoked(_)
5152 ) | (
5153 ProviderAdminJoinProgress::CleanupIntent { .. },
5154 ProviderAdminJoinProgress::Cancelled(_)
5155 ) | (
5156 ProviderAdminJoinProgress::CleanupIntent { .. },
5157 ProviderAdminJoinProgress::WriteRevoked(_)
5158 )
5159 )
5160}
5161
5162fn joiner_adjacent(previous: &JoinerJoinProgress, next: &JoinerJoinProgress) -> bool {
5163 matches!(
5164 (previous, next),
5165 (
5166 JoinerJoinProgress::OfferReceived(_),
5167 JoinerJoinProgress::AccessRequested(_)
5168 ) | (
5169 JoinerJoinProgress::AccessRequested(_),
5170 JoinerJoinProgress::ApprovalReceived(_)
5171 ) | (
5172 JoinerJoinProgress::AccessRequested(_),
5173 JoinerJoinProgress::Abandoned(_)
5174 ) | (
5175 JoinerJoinProgress::ApprovalReceived(_),
5176 JoinerJoinProgress::RegistrationPrepared(_)
5177 ) | (
5178 JoinerJoinProgress::ApprovalReceived(_),
5179 JoinerJoinProgress::Abandoned(_)
5180 ) | (
5181 JoinerJoinProgress::RegistrationPrepared(_),
5182 JoinerJoinProgress::ProviderReady(_)
5183 ) | (
5184 JoinerJoinProgress::RegistrationPrepared(_),
5185 JoinerJoinProgress::CleanupIntent { .. }
5186 ) | (
5187 JoinerJoinProgress::ProviderReady(_),
5188 JoinerJoinProgress::RegistrationCreateIntent(_)
5189 ) | (
5190 JoinerJoinProgress::ProviderReady(_),
5191 JoinerJoinProgress::CleanupIntent { .. }
5192 ) | (
5193 JoinerJoinProgress::RegistrationCreateIntent(_),
5194 JoinerJoinProgress::RegistrationCreated(_)
5195 ) | (
5196 JoinerJoinProgress::RegistrationCreateIntent(_),
5197 JoinerJoinProgress::CleanupIntent { .. }
5198 ) | (
5199 JoinerJoinProgress::RegistrationCreated(_),
5200 JoinerJoinProgress::AckCreateIntent(_)
5201 ) | (
5202 JoinerJoinProgress::RegistrationCreated(_),
5203 JoinerJoinProgress::CleanupIntent { .. }
5204 ) | (
5205 JoinerJoinProgress::AckCreateIntent(_),
5206 JoinerJoinProgress::AckCreated(_)
5207 ) | (
5208 JoinerJoinProgress::AckCreateIntent(_),
5209 JoinerJoinProgress::CleanupIntent { .. }
5210 ) | (
5211 JoinerJoinProgress::AckCreated(_),
5212 JoinerJoinProgress::ResponseCreateIntent(_)
5213 ) | (
5214 JoinerJoinProgress::AckCreated(_),
5215 JoinerJoinProgress::Ready(_)
5216 ) | (
5217 JoinerJoinProgress::AckCreated(_),
5218 JoinerJoinProgress::CleanupIntent { .. }
5219 ) | (
5220 JoinerJoinProgress::ResponseCreateIntent(_),
5221 JoinerJoinProgress::Ready(_)
5222 ) | (
5223 JoinerJoinProgress::ResponseCreateIntent(_),
5224 JoinerJoinProgress::CleanupIntent { .. }
5225 ) | (
5226 JoinerJoinProgress::Ready(_),
5227 JoinerJoinProgress::ActivationObserved(_)
5228 ) | (
5229 JoinerJoinProgress::Ready(_),
5230 JoinerJoinProgress::CleanupIntent { .. }
5231 ) | (
5232 JoinerJoinProgress::RegistrationPrepared(_),
5233 JoinerJoinProgress::WriteRevoked(_)
5234 ) | (
5235 JoinerJoinProgress::ProviderReady(_),
5236 JoinerJoinProgress::WriteRevoked(_)
5237 ) | (
5238 JoinerJoinProgress::RegistrationCreateIntent(_),
5239 JoinerJoinProgress::WriteRevoked(_)
5240 ) | (
5241 JoinerJoinProgress::RegistrationCreated(_),
5242 JoinerJoinProgress::WriteRevoked(_)
5243 ) | (
5244 JoinerJoinProgress::AckCreateIntent(_),
5245 JoinerJoinProgress::WriteRevoked(_)
5246 ) | (
5247 JoinerJoinProgress::AckCreated(_),
5248 JoinerJoinProgress::WriteRevoked(_)
5249 ) | (
5250 JoinerJoinProgress::ResponseCreateIntent(_),
5251 JoinerJoinProgress::WriteRevoked(_)
5252 ) | (
5253 JoinerJoinProgress::CleanupIntent { .. },
5254 JoinerJoinProgress::Cancelled(_)
5255 ) | (
5256 JoinerJoinProgress::ActivationObserved(_),
5257 JoinerJoinProgress::Activated(_)
5258 ) | (
5259 JoinerJoinProgress::Cancelled(_),
5260 JoinerJoinProgress::CleanupActivated(_)
5261 ) | (
5262 JoinerJoinProgress::WriteRevoked(_),
5263 JoinerJoinProgress::CleanupActivated(_)
5264 ) | (
5265 JoinerJoinProgress::CleanupActivated(_),
5266 JoinerJoinProgress::CancelledComplete(_)
5267 )
5268 )
5269}
5270
5271fn validate_initial_progress(progress: &DeviceJoinRoleProgress) -> Result<(), DeviceJoinError> {
5272 if matches!(
5273 progress,
5274 DeviceJoinRoleProgress::Owner(OwnerJoinProgress::Offered(_))
5275 | DeviceJoinRoleProgress::ProviderAdministrator(
5276 ProviderAdminJoinProgress::AccessRequested(_)
5277 )
5278 | DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::OfferReceived(_))
5279 ) {
5280 Ok(())
5281 } else {
5282 Err(DeviceJoinError::NonAdjacentJournalTransition)
5283 }
5284}
5285
5286fn require_distinct_slots(slots: &[ObjectSlot]) -> Result<(), DeviceJoinError> {
5287 let unique = slots.iter().collect::<BTreeSet<_>>();
5288 if unique.len() == slots.len() {
5289 Ok(())
5290 } else {
5291 Err(DeviceJoinError::DuplicateReservedSlot)
5292 }
5293}
5294
5295fn attempt_key(attempt_id: DeviceJoinAttemptId) -> String {
5296 serde_json::to_value(attempt_id)
5297 .expect("device join attempt id serialization cannot fail")
5298 .as_str()
5299 .expect("device join attempt id serializes as a string")
5300 .to_string()
5301}
5302
5303fn sign<T: Serialize>(signer: &UserKeypair, domain: &[u8], value: &T) -> String {
5304 let digest = ObjectHash::digest(&domain_json(domain, value));
5305 hex::encode(signer.sign(digest.as_bytes()))
5306}
5307
5308fn verify_signature<T: Serialize>(
5309 public_key: &str,
5310 signature: &str,
5311 domain: &[u8],
5312 value: &T,
5313) -> Result<(), DeviceJoinError> {
5314 let digest = ObjectHash::digest(&domain_json(domain, value));
5315 if keys::verify_signature_hex(public_key, signature, digest.as_bytes()) {
5316 Ok(())
5317 } else {
5318 Err(DeviceJoinError::InvalidSignature)
5319 }
5320}
5321
5322fn domain_json<T: Serialize>(domain: &[u8], value: &T) -> Vec<u8> {
5323 let mut bytes = domain.to_vec();
5324 bytes.extend(serde_json::to_vec(value).expect("closed device join serialization cannot fail"));
5325 bytes
5326}
5327
5328#[derive(Debug, thiserror::Error)]
5329pub enum DeviceJoinError {
5330 #[error("device join signature is invalid")]
5331 InvalidSignature,
5332 #[error("device join offer does not name one active Store/member/provider authority")]
5333 OfferMismatch,
5334 #[error(
5335 "device provider admission approval differs from its request or activated access grant"
5336 )]
5337 ApprovalMismatch,
5338 #[error("device registration request differs from its offer, approval, or reserved slots")]
5339 RegistrationRequestMismatch,
5340 #[error("device join attempt differs from its signed exchange")]
5341 AttemptMismatch,
5342 #[error("device join cleanup does not contain the unconditional canonical slot set")]
5343 CleanupMismatch,
5344 #[error("device join journal transition is not the declared adjacent transition")]
5345 NonAdjacentJournalTransition,
5346 #[error("device join journal has a different durable value for this role and attempt")]
5347 JournalConflict,
5348 #[error("pending join payload hash differs from the exact transferred payload")]
5349 PendingTransferHashMismatch,
5350 #[error("device join reserved slots are not pairwise distinct")]
5351 DuplicateReservedSlot,
5352 #[error("device join requires an existing Member identity")]
5353 MemberNotEligible,
5354 #[error("provider operation failed: {0}")]
5355 Provider(String),
5356 #[error("Store device join state: {0}")]
5357 Store(String),
5358 #[error("device join requires an activated local Store device")]
5359 ActiveDeviceRequired,
5360 #[error("device join requires the active local Owner authority")]
5361 OwnerAuthorityRequired,
5362 #[error("device join requires the selected effective provider administrator")]
5363 ProviderAdministratorRequired,
5364 #[error("device join requires resolved Store membership")]
5365 MembershipConflict,
5366 #[error("device join requires the provider's exact-slot adapter")]
5367 ExactSlotStorageRequired,
5368 #[error("device join attempt cut does not include its provider-access activation")]
5369 ApprovalActivationMissing,
5370 #[error("device join activation is not materialized in the installed Store database")]
5371 ActivationNotMaterialized,
5372 #[error(transparent)]
5373 Object(#[from] crate::sync::store_objects::StoreObjectError),
5374 #[error(transparent)]
5375 Registration(#[from] crate::sync::store_registration::StoreRegistrationError),
5376 #[error(transparent)]
5377 Pull(#[from] crate::sync::store_pull::StorePullError),
5378 #[error(transparent)]
5379 Outbound(#[from] crate::sync::store_outbound::StoreOutboundError),
5380 #[error(transparent)]
5381 Protocol(#[from] crate::sync::store_commit::StoreProtocolError),
5382 #[error(transparent)]
5383 Storage(#[from] crate::sync::storage::StorageError),
5384 #[error(transparent)]
5385 Database(#[from] rusqlite::Error),
5386 #[error(transparent)]
5387 Serialization(#[from] serde_json::Error),
5388}