1use super::cleanup::{require_cancelled_outcome, validate_terminals};
2use super::journal::require_distinct_slots;
3use super::*;
4
5const OFFER_DOMAIN: &[u8] = b"coven.device-join-offer.v1\0";
6const ACCESS_REQUEST_DOMAIN: &[u8] = b"coven.device-provider-access-request.v1\0";
7const APPROVAL_DOMAIN: &[u8] = b"coven.device-provider-admission-approval.v1\0";
8const REGISTRATION_REQUEST_DOMAIN: &[u8] = b"coven.device-registration-request.v1\0";
9const ABANDONMENT_DOMAIN: &[u8] = b"coven.device-join-abandonment.v1\0";
10const PROVIDER_CLOSURE_DOMAIN: &[u8] = b"coven.device-join-provider-closure.v1\0";
11const JOINER_CLOSURE_DOMAIN: &[u8] = b"coven.device-join-joiner-closure.v1\0";
12const WRITE_REVOCATION_DOMAIN: &[u8] = b"coven.device-join-write-revocation.v1\0";
13const CLEANUP_RECEIPT_DOMAIN: &[u8] = b"coven.device-join-cleanup-receipt.v1\0";
14
15#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
16#[serde(deny_unknown_fields)]
17pub struct DeviceJoinOffer {
18 pub version: u32,
19 pub attempt_id: DeviceJoinAttemptId,
20 pub member_pubkey: String,
21 pub store_root: StoreRootRef,
22 pub provider: StoreProviderBinding,
23 pub attempt_slot: ObjectSlot,
24 pub outcome_slot: ObjectSlot,
25 pub owner_registration: StoreDeviceRegistrationRef,
26 pub owner_grant: MembershipGrantId,
27 pub provider_admin: Box<ProviderAdminGrantRecord>,
28 pub signature: String,
29}
30
31impl DeviceJoinOffer {
32 #[allow(clippy::too_many_arguments)]
33 pub fn signed(
34 attempt_id: DeviceJoinAttemptId,
35 member_pubkey: String,
36 store_root: StoreRootRef,
37 provider: StoreProviderBinding,
38 attempt_slot: ObjectSlot,
39 outcome_slot: ObjectSlot,
40 owner_registration: StoreDeviceRegistrationRef,
41 owner_grant: MembershipGrantId,
42 provider_admin: ProviderAdminGrantRecord,
43 owner: &StoreDeviceRegistration,
44 owner_device_signer: &UserKeypair,
45 ) -> Result<Self, DeviceJoinError> {
46 owner_registration.verify_registration(owner)?;
47 if keys::public_key_hex(owner_device_signer) != owner.device_signing_pubkey {
48 return Err(DeviceJoinError::InvalidSignature);
49 }
50 let mut value = Self {
51 version: STORE_PROTOCOL_VERSION,
52 attempt_id,
53 member_pubkey,
54 store_root,
55 provider,
56 attempt_slot,
57 outcome_slot,
58 owner_registration,
59 owner_grant,
60 provider_admin: Box::new(provider_admin),
61 signature: String::new(),
62 };
63 value.validate_shape()?;
64 value.signature = sign(owner_device_signer, OFFER_DOMAIN, &value.signed_fields());
65 Ok(value)
66 }
67
68 pub fn verify(&self, owner: &StoreDeviceRegistration) -> Result<(), DeviceJoinError> {
69 self.validate_shape()?;
70 self.owner_registration.verify_registration(owner)?;
71 verify_signature(
72 &owner.device_signing_pubkey,
73 &self.signature,
74 OFFER_DOMAIN,
75 &self.signed_fields(),
76 )
77 }
78
79 pub fn offer_hash(&self) -> ObjectHash {
80 ObjectHash::digest(&domain_json(OFFER_DOMAIN, &self.signed_fields()))
81 }
82
83 fn validate_shape(&self) -> Result<(), DeviceJoinError> {
84 if self.version != STORE_PROTOCOL_VERSION
85 || self.member_pubkey.is_empty()
86 || self.provider_admin.administrator != self.owner_registration
87 && self.provider_admin.administrator.device_id == self.owner_registration.device_id
88 || self.attempt_slot == self.outcome_slot
89 {
90 return Err(DeviceJoinError::OfferMismatch);
91 }
92 self.provider.validate()?;
93 self.provider_admin
94 .provider
95 .validate_for(&self.provider)
96 .map_err(DeviceJoinError::Storage)?;
97 if let crate::sync::provider::ProviderAdminGrantOrigin::Founder { root } =
98 &self.provider_admin.created_at
99 {
100 if root != &self.store_root {
101 return Err(DeviceJoinError::OfferMismatch);
102 }
103 }
104 Ok(())
105 }
106
107 fn signed_fields(&self) -> DeviceJoinOfferFields<'_> {
108 DeviceJoinOfferFields {
109 version: self.version,
110 attempt_id: self.attempt_id,
111 member_pubkey: &self.member_pubkey,
112 store_root: &self.store_root,
113 provider: &self.provider,
114 attempt_slot: &self.attempt_slot,
115 outcome_slot: &self.outcome_slot,
116 owner_registration: &self.owner_registration,
117 owner_grant: &self.owner_grant,
118 provider_admin: &self.provider_admin,
119 }
120 }
121}
122
123#[derive(Serialize)]
124struct DeviceJoinOfferFields<'a> {
125 version: u32,
126 attempt_id: DeviceJoinAttemptId,
127 member_pubkey: &'a str,
128 store_root: &'a StoreRootRef,
129 provider: &'a StoreProviderBinding,
130 attempt_slot: &'a ObjectSlot,
131 outcome_slot: &'a ObjectSlot,
132 owner_registration: &'a StoreDeviceRegistrationRef,
133 owner_grant: &'a MembershipGrantId,
134 provider_admin: &'a ProviderAdminGrantRecord,
135}
136
137#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
138#[serde(deny_unknown_fields)]
139pub struct DeviceProviderAccessRequest {
140 pub offer: Box<DeviceJoinOffer>,
141 pub peer_provider: ProviderDeviceBinding,
142 pub signature: String,
143}
144
145impl DeviceProviderAccessRequest {
146 pub fn signed(
147 offer: DeviceJoinOffer,
148 peer_provider: ProviderDeviceBinding,
149 member_signer: &UserKeypair,
150 ) -> Result<Self, DeviceJoinError> {
151 if keys::public_key_hex(member_signer) != offer.member_pubkey {
152 return Err(DeviceJoinError::InvalidSignature);
153 }
154 peer_provider.validate_for(&offer.provider)?;
155 let mut request = Self {
156 offer: Box::new(offer),
157 peer_provider,
158 signature: String::new(),
159 };
160 request.signature = sign(
161 member_signer,
162 ACCESS_REQUEST_DOMAIN,
163 &request.signed_fields(),
164 );
165 Ok(request)
166 }
167
168 pub fn verify(&self, owner: &StoreDeviceRegistration) -> Result<(), DeviceJoinError> {
169 self.offer.verify(owner)?;
170 self.peer_provider.validate_for(&self.offer.provider)?;
171 verify_signature(
172 &self.offer.member_pubkey,
173 &self.signature,
174 ACCESS_REQUEST_DOMAIN,
175 &self.signed_fields(),
176 )
177 }
178
179 pub fn request_hash(&self) -> ObjectHash {
180 ObjectHash::digest(&domain_json(ACCESS_REQUEST_DOMAIN, &self.signed_fields()))
181 }
182
183 fn signed_fields(&self) -> (&DeviceJoinOffer, &ProviderDeviceBinding) {
184 (&self.offer, &self.peer_provider)
185 }
186}
187
188#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
189#[serde(rename_all = "snake_case", deny_unknown_fields)]
190pub enum DeviceProviderAdmissionChallenge {
191 SamePrincipal,
192 CrossPrincipal(CrossPrincipalProbeChallenge),
193}
194
195#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
196#[serde(deny_unknown_fields)]
197pub struct DeviceProviderAdmissionApproval {
198 pub request: Box<DeviceProviderAccessRequest>,
199 pub access_grant: ActivatedStoreMemberProviderAccessGrant,
200 pub admission: DeviceProviderAdmissionChallenge,
201 pub signature: String,
202}
203
204impl DeviceProviderAdmissionApproval {
205 pub(crate) fn signed(
206 request: DeviceProviderAccessRequest,
207 access_grant: ActivatedStoreMemberProviderAccessGrant,
208 admission: DeviceProviderAdmissionChallenge,
209 store_root: &crate::sync::store_objects::VerifiedObject<StoreProtocolRoot>,
210 administrator: &StoreDeviceRegistration,
211 administrator_device_signer: &UserKeypair,
212 ) -> Result<Self, DeviceJoinError> {
213 if keys::public_key_hex(administrator_device_signer) != administrator.device_signing_pubkey
214 {
215 return Err(DeviceJoinError::InvalidSignature);
216 }
217 let mut value = Self {
218 request: Box::new(request),
219 access_grant,
220 admission,
221 signature: String::new(),
222 };
223 value.validate_shape(store_root, administrator)?;
224 value.sign_with(administrator_device_signer);
225 Ok(value)
226 }
227
228 #[cfg(test)]
229 pub(crate) fn signed_without_shape_validation_for_test(
230 request: DeviceProviderAccessRequest,
231 access_grant: ActivatedStoreMemberProviderAccessGrant,
232 admission: DeviceProviderAdmissionChallenge,
233 administrator_device_signer: &UserKeypair,
234 ) -> Self {
235 let mut value = Self {
236 request: Box::new(request),
237 access_grant,
238 admission,
239 signature: String::new(),
240 };
241 value.sign_with(administrator_device_signer);
242 value
243 }
244
245 fn sign_with(&mut self, administrator_device_signer: &UserKeypair) {
246 self.signature = sign(
247 administrator_device_signer,
248 APPROVAL_DOMAIN,
249 &self.signed_fields(),
250 );
251 }
252
253 pub(in crate::sync::store) fn verify(
254 &self,
255 store_root: &crate::sync::store_objects::VerifiedObject<StoreProtocolRoot>,
256 owner: &StoreDeviceRegistration,
257 administrator: &StoreDeviceRegistration,
258 ) -> Result<(), DeviceJoinError> {
259 self.request.verify(owner)?;
260 self.validate_shape(store_root, administrator)?;
261 verify_signature(
262 &administrator.device_signing_pubkey,
263 &self.signature,
264 APPROVAL_DOMAIN,
265 &self.signed_fields(),
266 )
267 }
268
269 fn validate_shape(
270 &self,
271 store_root: &crate::sync::store_objects::VerifiedObject<StoreProtocolRoot>,
272 administrator: &StoreDeviceRegistration,
273 ) -> Result<(), DeviceJoinError> {
274 let offer = &self.request.offer;
275 if store_root.object != offer.store_root.object
276 || store_root.value.object_hash() != offer.store_root.store_root_hash
277 || store_root.value.descriptor.store_root_id() != offer.store_root.store_root_id
278 || store_root.value.descriptor.provider != offer.provider
279 || self.access_grant.grant.member_pubkey != offer.member_pubkey
280 || self.access_grant.grant.provider != self.request.peer_provider
281 || self.access_grant.grant_ref.grant_id != self.access_grant.grant.grant_id
282 || self.access_grant.grant_ref.grant_hash != self.access_grant.grant.grant_hash()
283 || self.access_grant.grant.administrator_grant != offer.provider_admin.grant_id
284 || self.access_grant.grant.administrator != offer.provider_admin.administrator
285 {
286 return Err(DeviceJoinError::ApprovalMismatch);
287 }
288 self.access_grant
289 .grant
290 .verify(&offer.provider, administrator)
291 .map_err(|error| DeviceJoinError::Provider(error.to_string()))?;
292 let same_principal = offer.provider_admin.provider == self.request.peer_provider;
293 if same_principal
294 != matches!(
295 self.admission,
296 DeviceProviderAdmissionChallenge::SamePrincipal
297 )
298 {
299 return Err(DeviceJoinError::ApprovalMismatch);
300 }
301 Ok(())
302 }
303
304 fn signed_fields(
305 &self,
306 ) -> (
307 &DeviceProviderAccessRequest,
308 &ActivatedStoreMemberProviderAccessGrant,
309 &DeviceProviderAdmissionChallenge,
310 ) {
311 (&self.request, &self.access_grant, &self.admission)
312 }
313}
314
315#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
316#[serde(rename_all = "snake_case", deny_unknown_fields)]
317pub enum DeviceProviderResponseReservation {
318 SamePrincipal,
319 CrossPrincipal { response_slot: ObjectSlot },
320}
321
322#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
323#[serde(deny_unknown_fields)]
324pub struct DeviceRegistrationRequest {
325 pub approval: Box<DeviceProviderAdmissionApproval>,
326 pub expected_registration: StoreDeviceRegistration,
327 pub registration_slot: ObjectSlot,
328 pub response: DeviceProviderResponseReservation,
329 pub signature: String,
330}
331
332impl DeviceRegistrationRequest {
333 pub fn signed(
334 approval: DeviceProviderAdmissionApproval,
335 expected_registration: StoreDeviceRegistration,
336 registration_slot: ObjectSlot,
337 response: DeviceProviderResponseReservation,
338 member_signer: &UserKeypair,
339 ) -> Result<Self, DeviceJoinError> {
340 if keys::public_key_hex(member_signer) != approval.request.offer.member_pubkey {
341 return Err(DeviceJoinError::InvalidSignature);
342 }
343 let mut value = Self {
344 approval: Box::new(approval),
345 expected_registration,
346 registration_slot,
347 response,
348 signature: String::new(),
349 };
350 value.validate_shape()?;
351 value.signature = sign(
352 member_signer,
353 REGISTRATION_REQUEST_DOMAIN,
354 &value.signed_fields(),
355 );
356 Ok(value)
357 }
358
359 pub fn verify(&self) -> Result<(), DeviceJoinError> {
360 self.validate_shape()?;
361 verify_signature(
362 &self.approval.request.offer.member_pubkey,
363 &self.signature,
364 REGISTRATION_REQUEST_DOMAIN,
365 &self.signed_fields(),
366 )
367 }
368
369 fn validate_shape(&self) -> Result<(), DeviceJoinError> {
370 let offer = &self.approval.request.offer;
371 if self.expected_registration.store_root != offer.store_root
372 || self.expected_registration.author_pubkey != offer.member_pubkey
373 || self.expected_registration.provider != self.approval.request.peer_provider
374 || self.registration_slot == offer.attempt_slot
375 || self.registration_slot == offer.outcome_slot
376 {
377 return Err(DeviceJoinError::RegistrationRequestMismatch);
378 }
379 match (
380 &self.approval.admission,
381 &self.response,
382 &self.expected_registration.origin,
383 ) {
384 (
385 DeviceProviderAdmissionChallenge::SamePrincipal,
386 DeviceProviderResponseReservation::SamePrincipal,
387 crate::sync::store_commit::StoreDeviceRegistrationOrigin::Join {
388 attempt_id,
389 attempt_slot,
390 outcome_slot,
391 },
392 )
393 | (
394 DeviceProviderAdmissionChallenge::CrossPrincipal(_),
395 DeviceProviderResponseReservation::CrossPrincipal { .. },
396 crate::sync::store_commit::StoreDeviceRegistrationOrigin::Join {
397 attempt_id,
398 attempt_slot,
399 outcome_slot,
400 },
401 ) if *attempt_id == offer.attempt_id
402 && attempt_slot == &offer.attempt_slot
403 && outcome_slot == &offer.outcome_slot => {}
404 _ => return Err(DeviceJoinError::RegistrationRequestMismatch),
405 }
406 let mut slots = vec![
407 offer.attempt_slot.clone(),
408 offer.outcome_slot.clone(),
409 self.registration_slot.clone(),
410 self.expected_registration
411 .acknowledgements
412 .first_slot()
413 .clone(),
414 ];
415 if let DeviceProviderAdmissionChallenge::CrossPrincipal(challenge) =
416 &self.approval.admission
417 {
418 slots.push(challenge.administrator_object.slot.clone());
419 }
420 if let DeviceProviderResponseReservation::CrossPrincipal { response_slot } = &self.response
421 {
422 slots.push(response_slot.clone());
423 }
424 require_distinct_slots(&slots)?;
425 Ok(())
426 }
427
428 fn signed_fields(
429 &self,
430 ) -> (
431 &DeviceProviderAdmissionApproval,
432 &StoreDeviceRegistration,
433 &ObjectSlot,
434 &DeviceProviderResponseReservation,
435 ) {
436 (
437 &self.approval,
438 &self.expected_registration,
439 &self.registration_slot,
440 &self.response,
441 )
442 }
443}
444
445#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
446#[serde(deny_unknown_fields)]
447pub struct ProvisionalDeviceBootstrap {
448 pub request: Box<DeviceRegistrationRequest>,
449 pub publication_authorization: DeviceJoinChallengePublicationAuthorization,
450}
451
452#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
453#[serde(rename_all = "snake_case", deny_unknown_fields)]
454pub enum DeviceProviderChallengePublication {
455 SamePrincipal,
456 CrossPrincipal {
457 challenge: CrossPrincipalProbeChallenge,
458 },
459}
460
461#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
462#[serde(deny_unknown_fields)]
463pub struct ProviderReadyDeviceBootstrap {
464 pub bootstrap: Box<ProvisionalDeviceBootstrap>,
465 pub challenge_publication: DeviceProviderChallengePublication,
466}
467
468#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
469#[serde(rename_all = "snake_case", deny_unknown_fields)]
470pub enum DeviceProviderReadiness {
471 SamePrincipal,
472 CrossPrincipal(CrossPrincipalProbeResponse),
473}
474
475#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
476#[serde(deny_unknown_fields)]
477pub struct DeviceJoinReadiness {
478 pub proof: DeviceReadinessProof,
479 pub provider: DeviceProviderReadiness,
480}
481
482#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
483#[serde(rename_all = "snake_case", deny_unknown_fields)]
484pub enum DeviceProviderAdmission {
485 SamePrincipal,
486 CrossPrincipal(CrossPrincipalProbeReceipt),
487}
488
489#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
490#[serde(deny_unknown_fields)]
491pub struct DeviceProviderAdmissionCompletion {
492 pub readiness: Box<DeviceJoinReadiness>,
493 pub admission: DeviceProviderAdmission,
494}
495
496#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
497#[serde(deny_unknown_fields)]
498pub struct DeviceJoinActivation {
499 pub outcome: DeviceJoinOutcomeRef,
500 pub outcome_activation: StoreBatchCommitRef,
501}
502
503#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
504#[serde(deny_unknown_fields)]
505pub struct DeviceJoinCancellation {
506 pub outcome: DeviceJoinOutcomeRef,
507 pub outcome_activation: StoreBatchCommitRef,
508}
509
510#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
511#[serde(deny_unknown_fields)]
512pub struct DeviceJoinAbandonmentRef {
513 pub attempt_id: DeviceJoinAttemptId,
514 pub abandonment_hash: ObjectHash,
515 pub object: ExactObjectRef,
516}
517
518impl DeviceJoinAbandonmentRef {
519 pub(in crate::sync::store) fn verify(
520 &self,
521 abandonment: &DeviceJoinAbandonmentObject,
522 owner: &StoreDeviceRegistration,
523 ) -> Result<(), DeviceJoinError> {
524 abandonment.owner_registration.verify_registration(owner)?;
525 if self.attempt_id != abandonment.attempt_id
526 || self.abandonment_hash != abandonment.abandonment_hash()
527 {
528 return Err(DeviceJoinError::AttemptMismatch);
529 }
530 verify_signature(
531 &owner.device_signing_pubkey,
532 &abandonment.signature,
533 ABANDONMENT_DOMAIN,
534 &abandonment.signed_fields(),
535 )
536 }
537}
538
539#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
540#[serde(deny_unknown_fields)]
541pub(in crate::sync::store) struct DeviceJoinAbandonmentObject {
542 pub version: u32,
543 pub store_root_hash: ObjectHash,
544 pub offer_hash: ObjectHash,
545 pub attempt_id: DeviceJoinAttemptId,
546 pub attempt_slot: ObjectSlot,
547 pub owner_registration: StoreDeviceRegistrationRef,
548 pub owner_grant: MembershipGrantId,
549 pub signature: String,
550}
551
552impl DeviceJoinAbandonmentObject {
553 pub(in crate::sync::store) fn signed(
554 offer: &DeviceJoinOffer,
555 owner: &StoreDeviceRegistration,
556 owner_device_signer: &UserKeypair,
557 ) -> Result<Self, DeviceJoinError> {
558 offer.verify(owner)?;
559 if keys::public_key_hex(owner_device_signer) != owner.device_signing_pubkey {
560 return Err(DeviceJoinError::InvalidSignature);
561 }
562 let mut value = Self {
563 version: STORE_PROTOCOL_VERSION,
564 store_root_hash: offer.store_root.store_root_hash,
565 offer_hash: offer.offer_hash(),
566 attempt_id: offer.attempt_id,
567 attempt_slot: offer.attempt_slot.clone(),
568 owner_registration: offer.owner_registration.clone(),
569 owner_grant: offer.owner_grant.clone(),
570 signature: String::new(),
571 };
572 value.signature = sign(
573 owner_device_signer,
574 ABANDONMENT_DOMAIN,
575 &value.signed_fields(),
576 );
577 Ok(value)
578 }
579
580 pub(in crate::sync::store) fn abandonment_hash(&self) -> ObjectHash {
581 ObjectHash::digest(&domain_json(ABANDONMENT_DOMAIN, &self.signed_fields()))
582 }
583
584 pub(in crate::sync::store) fn to_bytes(&self) -> Vec<u8> {
585 serde_json::to_vec(self).expect("device join abandonment serialization cannot fail")
586 }
587
588 fn signed_fields(
589 &self,
590 ) -> (
591 u32,
592 ObjectHash,
593 ObjectHash,
594 DeviceJoinAttemptId,
595 &ObjectSlot,
596 &StoreDeviceRegistrationRef,
597 &MembershipGrantId,
598 ) {
599 (
600 self.version,
601 self.store_root_hash,
602 self.offer_hash,
603 self.attempt_id,
604 &self.attempt_slot,
605 &self.owner_registration,
606 &self.owner_grant,
607 )
608 }
609}
610
611#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
612#[serde(deny_unknown_fields)]
613pub struct DeviceJoinAbandonment {
614 pub abandonment: DeviceJoinAbandonmentRef,
615 pub abandonment_activation: StoreBatchCommitRef,
616}
617
618#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
619#[serde(rename_all = "snake_case", deny_unknown_fields)]
620pub enum ProviderChallengeDisposition {
621 SamePrincipal,
622 NeverCreated,
623 Created(ExactObjectRef),
624 AlreadyDeleted(ExactObjectRef),
625}
626
627#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
628#[serde(deny_unknown_fields)]
629pub struct ProviderAdminJoinClosure {
630 pub cancellation: DeviceJoinOutcomeRef,
631 pub administrator_registration: StoreDeviceRegistrationRef,
632 pub challenge: ProviderChallengeDisposition,
633 pub prior_state_hash: ObjectHash,
634 pub signature: String,
635}
636
637impl ProviderAdminJoinClosure {
638 pub fn signed(
639 cancellation: DeviceJoinOutcomeRef,
640 administrator_registration: StoreDeviceRegistrationRef,
641 challenge: ProviderChallengeDisposition,
642 prior_state_hash: ObjectHash,
643 administrator: &StoreDeviceRegistration,
644 signer: &UserKeypair,
645 ) -> Result<Self, DeviceJoinError> {
646 require_cancelled_outcome(&cancellation)?;
647 administrator_registration.verify_registration(administrator)?;
648 if keys::public_key_hex(signer) != administrator.device_signing_pubkey {
649 return Err(DeviceJoinError::InvalidSignature);
650 }
651 let mut value = Self {
652 cancellation,
653 administrator_registration,
654 challenge,
655 prior_state_hash,
656 signature: String::new(),
657 };
658 value.signature = sign(signer, PROVIDER_CLOSURE_DOMAIN, &value.signed_fields());
659 Ok(value)
660 }
661
662 pub fn verify(&self, administrator: &StoreDeviceRegistration) -> Result<(), DeviceJoinError> {
663 require_cancelled_outcome(&self.cancellation)?;
664 self.administrator_registration
665 .verify_registration(administrator)?;
666 verify_signature(
667 &administrator.device_signing_pubkey,
668 &self.signature,
669 PROVIDER_CLOSURE_DOMAIN,
670 &self.signed_fields(),
671 )
672 }
673
674 fn signed_fields(
675 &self,
676 ) -> (
677 &DeviceJoinOutcomeRef,
678 &StoreDeviceRegistrationRef,
679 &ProviderChallengeDisposition,
680 ObjectHash,
681 ) {
682 (
683 &self.cancellation,
684 &self.administrator_registration,
685 &self.challenge,
686 self.prior_state_hash,
687 )
688 }
689}
690
691#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
692#[serde(rename_all = "snake_case", deny_unknown_fields)]
693pub enum SlotDisposition {
694 NeverCreated,
695 Created(ExactObjectRef),
696}
697
698#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
699#[serde(rename_all = "snake_case", deny_unknown_fields)]
700pub enum JoinerResponseDisposition {
701 SamePrincipal,
702 Slot(SlotDisposition),
703}
704
705#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
706#[serde(deny_unknown_fields)]
707pub struct JoinerJoinClosure {
708 pub cancellation: DeviceJoinOutcomeRef,
709 pub expected_registration: StoreDeviceRegistration,
710 pub registration: SlotDisposition,
711 pub initial_ack: SlotDisposition,
712 pub response: JoinerResponseDisposition,
713 pub prior_state_hash: ObjectHash,
714 pub signature: String,
715}
716
717impl JoinerJoinClosure {
718 #[allow(clippy::too_many_arguments)]
719 pub fn signed(
720 cancellation: DeviceJoinOutcomeRef,
721 expected_registration: StoreDeviceRegistration,
722 registration: SlotDisposition,
723 initial_ack: SlotDisposition,
724 response: JoinerResponseDisposition,
725 prior_state_hash: ObjectHash,
726 signer: &UserKeypair,
727 ) -> Result<Self, DeviceJoinError> {
728 require_cancelled_outcome(&cancellation)?;
729 if keys::public_key_hex(signer) != expected_registration.device_signing_pubkey {
730 return Err(DeviceJoinError::InvalidSignature);
731 }
732 let mut value = Self {
733 cancellation,
734 expected_registration,
735 registration,
736 initial_ack,
737 response,
738 prior_state_hash,
739 signature: String::new(),
740 };
741 value.signature = sign(signer, JOINER_CLOSURE_DOMAIN, &value.signed_fields());
742 Ok(value)
743 }
744
745 pub fn verify(&self) -> Result<(), DeviceJoinError> {
746 require_cancelled_outcome(&self.cancellation)?;
747 verify_signature(
748 &self.expected_registration.device_signing_pubkey,
749 &self.signature,
750 JOINER_CLOSURE_DOMAIN,
751 &self.signed_fields(),
752 )
753 }
754
755 fn signed_fields(
756 &self,
757 ) -> (
758 &DeviceJoinOutcomeRef,
759 &StoreDeviceRegistration,
760 &SlotDisposition,
761 &SlotDisposition,
762 &JoinerResponseDisposition,
763 ObjectHash,
764 ) {
765 (
766 &self.cancellation,
767 &self.expected_registration,
768 &self.registration,
769 &self.initial_ack,
770 &self.response,
771 self.prior_state_hash,
772 )
773 }
774}
775
776#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
777#[serde(rename_all = "snake_case")]
778pub enum DeviceJoinProducer {
779 ProviderAdministrator,
780 Joiner,
781}
782
783#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
784#[serde(rename_all = "snake_case", deny_unknown_fields)]
785pub enum ProviderWriteAuthorityRef {
786 ProviderAdministrator(ProviderAdminGrantId),
787 MemberAccess(StoreMemberProviderAccessGrantRef),
788}
789
790#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
791#[serde(deny_unknown_fields)]
792pub struct DeviceJoinProducerWriteRevocation {
793 pub cancellation: DeviceJoinOutcomeRef,
794 pub producer: DeviceJoinProducer,
795 pub authority: ProviderWriteAuthorityRef,
796 pub protected_slots: Vec<ObjectSlot>,
797 pub withdrawal: ProviderAccessWithdrawal,
798 pub executor_grant: ProviderAdminGrantId,
799 pub executor: StoreDeviceRegistrationRef,
800 pub signature: String,
801}
802
803impl DeviceJoinProducerWriteRevocation {
804 pub fn signed(
805 cancellation: DeviceJoinOutcomeRef,
806 producer: DeviceJoinProducer,
807 authority: ProviderWriteAuthorityRef,
808 mut protected_slots: Vec<ObjectSlot>,
809 withdrawal: ProviderAccessWithdrawal,
810 executor_grant: ProviderAdminGrantId,
811 executor: StoreDeviceRegistrationRef,
812 executor_registration: &StoreDeviceRegistration,
813 executor_signer: &UserKeypair,
814 ) -> Result<Self, DeviceJoinError> {
815 require_cancelled_outcome(&cancellation)?;
816 executor.verify_registration(executor_registration)?;
817 if keys::public_key_hex(executor_signer) != executor_registration.device_signing_pubkey {
818 return Err(DeviceJoinError::InvalidSignature);
819 }
820 protected_slots.sort();
821 if protected_slots.is_empty() || protected_slots.windows(2).any(|pair| pair[0] == pair[1]) {
822 return Err(DeviceJoinError::CleanupMismatch);
823 }
824 let mut value = Self {
825 cancellation,
826 producer,
827 authority,
828 protected_slots,
829 withdrawal,
830 executor_grant,
831 executor,
832 signature: String::new(),
833 };
834 value.signature = sign(
835 executor_signer,
836 WRITE_REVOCATION_DOMAIN,
837 &value.signed_fields(),
838 );
839 Ok(value)
840 }
841
842 pub fn verify(&self, executor: &StoreDeviceRegistration) -> Result<(), DeviceJoinError> {
843 require_cancelled_outcome(&self.cancellation)?;
844 self.executor.verify_registration(executor)?;
845 verify_signature(
846 &executor.device_signing_pubkey,
847 &self.signature,
848 WRITE_REVOCATION_DOMAIN,
849 &self.signed_fields(),
850 )
851 }
852
853 fn signed_fields(
854 &self,
855 ) -> (
856 &DeviceJoinOutcomeRef,
857 DeviceJoinProducer,
858 &ProviderWriteAuthorityRef,
859 &[ObjectSlot],
860 &ProviderAccessWithdrawal,
861 &ProviderAdminGrantId,
862 &StoreDeviceRegistrationRef,
863 ) {
864 (
865 &self.cancellation,
866 self.producer,
867 &self.authority,
868 &self.protected_slots,
869 &self.withdrawal,
870 &self.executor_grant,
871 &self.executor,
872 )
873 }
874}
875
876#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
877#[serde(rename_all = "snake_case", deny_unknown_fields)]
878pub enum ProviderAdminJoinTerminal {
879 Completed(DeviceProviderAdmissionCompletion),
880 Cancelled(ProviderAdminJoinClosure),
881 WriteRevoked(DeviceJoinProducerWriteRevocation),
882}
883
884#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
885#[serde(rename_all = "snake_case", deny_unknown_fields)]
886pub enum JoinerJoinTerminal {
887 Ready(DeviceJoinReadiness),
888 Cancelled(JoinerJoinClosure),
889 WriteRevoked(DeviceJoinProducerWriteRevocation),
890}
891
892#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
893#[serde(deny_unknown_fields)]
894pub struct DeviceJoinCleanupReceiptRef {
895 pub attempt_id: DeviceJoinAttemptId,
896 pub receipt_hash: ObjectHash,
897 pub object: ExactObjectRef,
898}
899
900impl DeviceJoinCleanupReceiptRef {
901 pub(in crate::sync::store) fn verify(
902 &self,
903 receipt: &DeviceJoinCleanupReceiptObject,
904 executor: &StoreDeviceRegistration,
905 ) -> Result<(), DeviceJoinError> {
906 receipt.executor.verify_registration(executor)?;
907 if self.attempt_id != receipt.cancellation.attempt().attempt_id
908 || self.receipt_hash != receipt.receipt_hash()
909 {
910 return Err(DeviceJoinError::CleanupMismatch);
911 }
912 verify_signature(
913 &executor.device_signing_pubkey,
914 &receipt.signature,
915 CLEANUP_RECEIPT_DOMAIN,
916 &receipt.signed_fields(),
917 )
918 }
919}
920
921#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
922#[serde(deny_unknown_fields)]
923pub(in crate::sync::store) struct DeviceJoinCleanupReceiptObject {
924 pub version: u32,
925 pub store_root_hash: ObjectHash,
926 pub cancellation: DeviceJoinOutcomeRef,
927 pub administrator_terminal: ProviderAdminJoinTerminal,
928 pub joiner_terminal: JoinerJoinTerminal,
929 pub deleted_slots: Vec<ObjectSlot>,
930 pub membership: StoreMembershipStateRef,
931 pub provider_admin_grant: ProviderAdminGrantId,
932 pub executor: StoreDeviceRegistrationRef,
933 pub signature: String,
934}
935
936impl DeviceJoinCleanupReceiptObject {
937 #[allow(clippy::too_many_arguments)]
938 pub(in crate::sync::store) fn signed(
939 attempt: &DeviceJoinAttempt,
940 cancellation: DeviceJoinOutcomeRef,
941 administrator_terminal: ProviderAdminJoinTerminal,
942 joiner_terminal: JoinerJoinTerminal,
943 deleted_slots: Vec<ObjectSlot>,
944 membership: StoreMembershipStateRef,
945 provider_admin_grant: ProviderAdminGrantId,
946 executor: StoreDeviceRegistrationRef,
947 executor_registration: &StoreDeviceRegistration,
948 executor_signer: &UserKeypair,
949 ) -> Result<Self, DeviceJoinError> {
950 require_cancelled_outcome(&cancellation)?;
951 if cancellation.attempt().attempt_id != attempt.attempt_id {
952 return Err(DeviceJoinError::AttemptMismatch);
953 }
954 executor.verify_registration(executor_registration)?;
955 if executor_registration.store_root != attempt.store_root
956 || keys::public_key_hex(executor_signer) != executor_registration.device_signing_pubkey
957 {
958 return Err(DeviceJoinError::InvalidSignature);
959 }
960 let mut value = Self {
961 version: STORE_PROTOCOL_VERSION,
962 store_root_hash: attempt.store_root.store_root_hash,
963 cancellation,
964 administrator_terminal,
965 joiner_terminal,
966 deleted_slots,
967 membership,
968 provider_admin_grant,
969 executor,
970 signature: String::new(),
971 };
972 value.validate_shape(attempt)?;
973 value.signature = sign(
974 executor_signer,
975 CLEANUP_RECEIPT_DOMAIN,
976 &value.signed_fields(),
977 );
978 Ok(value)
979 }
980
981 pub(in crate::sync::store) fn receipt_hash(&self) -> ObjectHash {
982 ObjectHash::digest(&domain_json(CLEANUP_RECEIPT_DOMAIN, &self.signed_fields()))
983 }
984
985 pub(in crate::sync::store) fn verify(
986 &self,
987 attempt: &DeviceJoinAttempt,
988 executor: &StoreDeviceRegistration,
989 ) -> Result<(), DeviceJoinError> {
990 if self.version != STORE_PROTOCOL_VERSION
991 || self.store_root_hash != attempt.store_root.store_root_hash
992 {
993 return Err(DeviceJoinError::CleanupMismatch);
994 }
995 let mut verified = self.clone();
996 verified.validate_shape(attempt)?;
997 verify_signature(
998 &executor.device_signing_pubkey,
999 &self.signature,
1000 CLEANUP_RECEIPT_DOMAIN,
1001 &self.signed_fields(),
1002 )
1003 }
1004
1005 pub(in crate::sync::store) fn to_bytes(&self) -> Vec<u8> {
1006 serde_json::to_vec(self).expect("device join cleanup receipt serialization cannot fail")
1007 }
1008
1009 fn validate_shape(&mut self, attempt: &DeviceJoinAttempt) -> Result<(), DeviceJoinError> {
1010 validate_terminals(
1011 &self.cancellation,
1012 &self.administrator_terminal,
1013 &self.joiner_terminal,
1014 )?;
1015 let expected = canonical_cleanup_slots(attempt)?;
1016 self.deleted_slots.sort();
1017 if self.deleted_slots != expected {
1018 return Err(DeviceJoinError::CleanupMismatch);
1019 }
1020 Ok(())
1021 }
1022
1023 fn signed_fields(
1024 &self,
1025 ) -> (
1026 u32,
1027 ObjectHash,
1028 &DeviceJoinOutcomeRef,
1029 &ProviderAdminJoinTerminal,
1030 &JoinerJoinTerminal,
1031 &[ObjectSlot],
1032 &StoreMembershipStateRef,
1033 &ProviderAdminGrantId,
1034 &StoreDeviceRegistrationRef,
1035 ) {
1036 (
1037 self.version,
1038 self.store_root_hash,
1039 &self.cancellation,
1040 &self.administrator_terminal,
1041 &self.joiner_terminal,
1042 &self.deleted_slots,
1043 &self.membership,
1044 &self.provider_admin_grant,
1045 &self.executor,
1046 )
1047 }
1048}
1049
1050#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1051#[serde(deny_unknown_fields)]
1052pub struct DeviceJoinCleanupReceipt {
1053 pub receipt: DeviceJoinCleanupReceiptRef,
1054}
1055
1056#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1057#[serde(deny_unknown_fields)]
1058pub struct DeviceJoinCleanupActivation {
1059 pub receipt: DeviceJoinCleanupReceiptRef,
1060 pub activation: StoreBatchCommitRef,
1061}
1062
1063#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1064#[serde(deny_unknown_fields)]
1065pub struct JoinedStore {
1066 pub store_root: StoreRootRef,
1067 pub registration: StoreDeviceRegistrationRef,
1068 pub activation: DeviceJoinActivation,
1069}
1070
1071fn sign<T: Serialize>(signer: &UserKeypair, domain: &[u8], value: &T) -> String {
1072 let digest = ObjectHash::digest(&domain_json(domain, value));
1073 hex::encode(signer.sign(digest.as_bytes()))
1074}
1075
1076fn verify_signature<T: Serialize>(
1077 public_key: &str,
1078 signature: &str,
1079 domain: &[u8],
1080 value: &T,
1081) -> Result<(), DeviceJoinError> {
1082 let digest = ObjectHash::digest(&domain_json(domain, value));
1083 if keys::verify_signature_hex(public_key, signature, digest.as_bytes()) {
1084 Ok(())
1085 } else {
1086 Err(DeviceJoinError::InvalidSignature)
1087 }
1088}
1089
1090fn domain_json<T: Serialize>(domain: &[u8], value: &T) -> Vec<u8> {
1091 let mut bytes = domain.to_vec();
1092 bytes.extend(serde_json::to_vec(value).expect("closed device join serialization cannot fail"));
1093 bytes
1094}