1use super::validation::require_version;
2use super::*;
3
4const STORE_DEVICE_ID_DOMAIN: &[u8] = b"coven.store-device-id.v1\0";
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
11#[serde(transparent)]
12pub struct StoreDeviceId(ObjectHash);
13
14impl StoreDeviceId {
15 pub fn derive(store_root: &StoreRootRef, origin: &StoreDeviceRegistrationOrigin) -> Self {
16 let mut material = STORE_DEVICE_ID_DOMAIN.to_vec();
17 material.extend(
18 serde_json::to_vec(&(store_root, origin.external_id()))
19 .expect("Store device identity serialization cannot fail"),
20 );
21 Self(ObjectHash::digest(&material))
22 }
23}
24
25impl fmt::Display for StoreDeviceId {
26 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
27 fmt::Display::fmt(&self.0, formatter)
28 }
29}
30
31impl FromStr for StoreDeviceId {
32 type Err = StoreProtocolError;
33
34 fn from_str(value: &str) -> Result<Self, Self::Err> {
35 value.parse().map(Self)
36 }
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
40#[serde(transparent)]
41pub struct StoreCreationId(ObjectHash);
42
43impl StoreCreationId {
44 pub fn from_random_bytes(bytes: [u8; 32]) -> Self {
45 Self(ObjectHash::from_digest(bytes))
46 }
47
48 #[cfg(any(test, feature = "test-utils"))]
49 pub(crate) fn from_nonce(nonce: &str) -> Self {
50 Self(ObjectHash::digest(nonce.as_bytes()))
51 }
52
53 pub(super) fn object_hash(self) -> ObjectHash {
54 self.0
55 }
56}
57
58impl fmt::Display for StoreCreationId {
59 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
60 fmt::Display::fmt(&self.0, formatter)
61 }
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
65#[serde(transparent)]
66pub struct DeviceJoinAttemptId(ObjectHash);
67
68impl DeviceJoinAttemptId {
69 pub fn from_hash(hash: ObjectHash) -> Self {
70 Self(hash)
71 }
72
73 pub(super) fn object_hash(self) -> ObjectHash {
74 self.0
75 }
76}
77
78impl fmt::Display for DeviceJoinAttemptId {
79 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
80 fmt::Display::fmt(&self.0, formatter)
81 }
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
85#[serde(transparent)]
86pub struct DeviceRecoveryId(ObjectHash);
87
88impl DeviceRecoveryId {
89 pub fn from_hash(hash: ObjectHash) -> Self {
90 Self(hash)
91 }
92
93 pub(super) fn object_hash(self) -> ObjectHash {
94 self.0
95 }
96}
97
98impl fmt::Display for DeviceRecoveryId {
99 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
100 fmt::Display::fmt(&self.0, formatter)
101 }
102}
103
104#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
105#[serde(deny_unknown_fields)]
106pub struct DeviceJoinAttemptRef {
107 pub attempt_id: DeviceJoinAttemptId,
108 pub attempt_hash: ObjectHash,
109 pub object: ExactObjectRef,
110}
111
112#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
113#[serde(deny_unknown_fields)]
114pub struct DeviceJoinAttempt {
115 pub version: u32,
116 pub store_root: StoreRootRef,
117 pub attempt_id: DeviceJoinAttemptId,
118 pub attempt_slot: ObjectSlot,
119 pub expected_registration: StoreDeviceRegistration,
120 pub registration_slot: ObjectSlot,
121 pub outcome_slot: ObjectSlot,
122 pub bootstrap_cut: StoreHistoryCut,
123 pub membership: StoreMembershipStateRef,
124 pub provider_admin_grant: crate::sync::provider::ProviderAdminGrantId,
125 pub provider_approval: crate::sync::store::DeviceProviderAdmissionApproval,
126 pub provider_response: crate::sync::store::DeviceProviderResponseReservation,
127 pub owner_registration: StoreDeviceRegistrationRef,
128 pub owner_grant: MembershipGrantId,
129 pub signature: String,
130}
131
132pub(crate) struct UnverifiedDeviceJoinAttempt(DeviceJoinAttempt);
133
134impl UnverifiedDeviceJoinAttempt {
135 pub(crate) fn verify_at(
136 self,
137 expected: &DeviceJoinAttemptRef,
138 owner: &StoreDeviceRegistration,
139 ) -> Result<DeviceJoinAttempt, StoreProtocolError> {
140 let attempt = self.0;
141 require_version(attempt.version)?;
142 attempt.validate_shape()?;
143 if attempt.attempt_id != expected.attempt_id
144 || attempt.attempt_hash() != expected.attempt_hash
145 || &attempt.attempt_slot != expected.object.slot()
146 {
147 return Err(StoreProtocolError::JoinAttemptMismatch);
148 }
149 attempt.owner_registration.verify_registration(owner)?;
150 if !keys::verify_signature_hex(
151 &owner.device_signing_pubkey,
152 &attempt.signature,
153 &attempt.canonical_signed_bytes(),
154 ) {
155 return Err(StoreProtocolError::InvalidSignature);
156 }
157 Ok(attempt)
158 }
159}
160
161#[derive(Serialize)]
162struct DeviceJoinAttemptSignedFields<'a> {
163 version: u32,
164 store_root: &'a StoreRootRef,
165 attempt_id: DeviceJoinAttemptId,
166 attempt_slot: &'a ObjectSlot,
167 expected_registration: &'a StoreDeviceRegistration,
168 registration_slot: &'a ObjectSlot,
169 outcome_slot: &'a ObjectSlot,
170 bootstrap_cut: &'a StoreHistoryCut,
171 membership: &'a StoreMembershipStateRef,
172 provider_admin_grant: &'a crate::sync::provider::ProviderAdminGrantId,
173 provider_approval: &'a crate::sync::store::DeviceProviderAdmissionApproval,
174 provider_response: &'a crate::sync::store::DeviceProviderResponseReservation,
175 owner_registration: &'a StoreDeviceRegistrationRef,
176 owner_grant: &'a MembershipGrantId,
177}
178
179impl DeviceJoinAttempt {
180 #[allow(clippy::too_many_arguments)]
181 pub fn signed(
182 store_root: StoreRootRef,
183 attempt_id: DeviceJoinAttemptId,
184 attempt_slot: ObjectSlot,
185 expected_registration: StoreDeviceRegistration,
186 registration_slot: ObjectSlot,
187 outcome_slot: ObjectSlot,
188 bootstrap_cut: StoreHistoryCut,
189 membership: StoreMembershipStateRef,
190 provider_admin_grant: crate::sync::provider::ProviderAdminGrantId,
191 provider_approval: crate::sync::store::DeviceProviderAdmissionApproval,
192 provider_response: crate::sync::store::DeviceProviderResponseReservation,
193 owner_registration: StoreDeviceRegistrationRef,
194 owner_grant: MembershipGrantId,
195 owner: &StoreDeviceRegistration,
196 owner_device_signer: &UserKeypair,
197 ) -> Result<Self, StoreProtocolError> {
198 owner_registration.verify_registration(owner)?;
199 if keys::public_key_hex(owner_device_signer) != owner.device_signing_pubkey {
200 return Err(StoreProtocolError::InvalidSignature);
201 }
202 let mut attempt = Self {
203 version: STORE_PROTOCOL_VERSION,
204 store_root,
205 attempt_id,
206 attempt_slot,
207 expected_registration,
208 registration_slot,
209 outcome_slot,
210 bootstrap_cut,
211 membership,
212 provider_admin_grant,
213 provider_approval,
214 provider_response,
215 owner_registration,
216 owner_grant,
217 signature: String::new(),
218 };
219 attempt.validate_shape()?;
220 let (_, signature) = keys::sign_hex(owner_device_signer, &attempt.canonical_signed_bytes());
221 attempt.signature = signature;
222 Ok(attempt)
223 }
224
225 fn canonical_signed_bytes(&self) -> Vec<u8> {
226 domain_json(
227 DEVICE_JOIN_ATTEMPT_DOMAIN,
228 &DeviceJoinAttemptSignedFields {
229 version: self.version,
230 store_root: &self.store_root,
231 attempt_id: self.attempt_id,
232 attempt_slot: &self.attempt_slot,
233 expected_registration: &self.expected_registration,
234 registration_slot: &self.registration_slot,
235 outcome_slot: &self.outcome_slot,
236 bootstrap_cut: &self.bootstrap_cut,
237 membership: &self.membership,
238 provider_admin_grant: &self.provider_admin_grant,
239 provider_approval: &self.provider_approval,
240 provider_response: &self.provider_response,
241 owner_registration: &self.owner_registration,
242 owner_grant: &self.owner_grant,
243 },
244 )
245 }
246
247 pub fn attempt_hash(&self) -> ObjectHash {
248 ObjectHash::digest(&self.canonical_signed_bytes())
249 }
250
251 pub fn to_bytes(&self) -> Vec<u8> {
252 serde_json::to_vec(self).expect("DeviceJoinAttempt serialization cannot fail")
253 }
254
255 pub fn parse_at(
256 bytes: &[u8],
257 expected: &DeviceJoinAttemptRef,
258 owner: &StoreDeviceRegistration,
259 ) -> Result<Self, StoreProtocolError> {
260 Self::parse_unverified(bytes)?.verify_at(expected, owner)
261 }
262
263 pub(crate) fn parse_unverified(
264 bytes: &[u8],
265 ) -> Result<UnverifiedDeviceJoinAttempt, StoreProtocolError> {
266 serde_json::from_slice(bytes)
267 .map(UnverifiedDeviceJoinAttempt)
268 .map_err(|error| StoreProtocolError::Malformed(error.to_string()))
269 }
270
271 fn validate_shape(&self) -> Result<(), StoreProtocolError> {
272 validate_store_history_cut(&self.bootstrap_cut)?;
273 if self.expected_registration.store_root != self.store_root
274 || self.expected_registration.device_id
275 != StoreDeviceId::derive(&self.store_root, &self.expected_registration.origin)
276 || self.attempt_slot == self.registration_slot
277 || self.attempt_slot == self.outcome_slot
278 || self.registration_slot == self.outcome_slot
279 || self.provider_admin_grant
280 != self.provider_approval.request.offer.provider_admin.grant_id
281 || self.provider_approval.request.offer.store_root != self.store_root
282 || self.provider_approval.request.offer.attempt_id != self.attempt_id
283 || self.provider_approval.request.offer.attempt_slot != self.attempt_slot
284 || self.provider_approval.request.offer.outcome_slot != self.outcome_slot
285 || self.provider_approval.request.offer.owner_registration != self.owner_registration
286 || self.provider_approval.request.offer.owner_grant != self.owner_grant
287 || self.provider_approval.request.offer.member_pubkey
288 != self.expected_registration.author_pubkey
289 || self.provider_approval.request.peer_provider != self.expected_registration.provider
290 {
291 return Err(StoreProtocolError::JoinAttemptMismatch);
292 }
293 match (&self.provider_approval.admission, &self.provider_response) {
294 (
295 crate::sync::store::DeviceProviderAdmissionChallenge::SamePrincipal,
296 crate::sync::store::DeviceProviderResponseReservation::SamePrincipal,
297 )
298 | (
299 crate::sync::store::DeviceProviderAdmissionChallenge::CrossPrincipal(_),
300 crate::sync::store::DeviceProviderResponseReservation::CrossPrincipal { .. },
301 ) => {}
302 _ => return Err(StoreProtocolError::JoinAttemptMismatch),
303 }
304 match &self.expected_registration.origin {
305 StoreDeviceRegistrationOrigin::Join {
306 attempt_id,
307 attempt_slot,
308 outcome_slot,
309 } if *attempt_id == self.attempt_id
310 && attempt_slot == &self.attempt_slot
311 && outcome_slot == &self.outcome_slot =>
312 {
313 Ok(())
314 }
315 _ => Err(StoreProtocolError::JoinAttemptMismatch),
316 }
317 }
318}
319
320#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
321#[serde(deny_unknown_fields)]
322pub struct DeviceReadinessProof {
323 pub version: u32,
324 pub store_root_hash: ObjectHash,
325 pub attempt: DeviceJoinAttemptRef,
326 pub registration: StoreDeviceRegistrationRef,
327 pub initial_ack: StoreAckRef,
328 pub bootstrap_cut: StoreHistoryCut,
329 pub signature: String,
330}
331
332#[derive(Serialize)]
333struct DeviceReadinessSignedFields<'a> {
334 version: u32,
335 store_root_hash: ObjectHash,
336 attempt: &'a DeviceJoinAttemptRef,
337 registration: &'a StoreDeviceRegistrationRef,
338 initial_ack: &'a StoreAckRef,
339 bootstrap_cut: &'a StoreHistoryCut,
340}
341
342impl DeviceReadinessProof {
343 pub fn signed(
344 attempt: DeviceJoinAttemptRef,
345 registration: StoreDeviceRegistrationRef,
346 initial_ack: StoreAckRef,
347 bootstrap_cut: StoreHistoryCut,
348 registration_value: &StoreDeviceRegistration,
349 device_signer: &UserKeypair,
350 ) -> Result<Self, StoreProtocolError> {
351 registration.verify_registration(registration_value)?;
352 if keys::public_key_hex(device_signer) != registration_value.device_signing_pubkey {
353 return Err(StoreProtocolError::InvalidSignature);
354 }
355 let mut proof = Self {
356 version: STORE_PROTOCOL_VERSION,
357 store_root_hash: registration_value.store_root.store_root_hash,
358 attempt,
359 registration,
360 initial_ack,
361 bootstrap_cut,
362 signature: String::new(),
363 };
364 validate_store_history_cut(&proof.bootstrap_cut)?;
365 let (_, signature) = keys::sign_hex(device_signer, &proof.canonical_signed_bytes());
366 proof.signature = signature;
367 Ok(proof)
368 }
369
370 fn canonical_signed_bytes(&self) -> Vec<u8> {
371 domain_json(
372 DEVICE_READINESS_DOMAIN,
373 &DeviceReadinessSignedFields {
374 version: self.version,
375 store_root_hash: self.store_root_hash,
376 attempt: &self.attempt,
377 registration: &self.registration,
378 initial_ack: &self.initial_ack,
379 bootstrap_cut: &self.bootstrap_cut,
380 },
381 )
382 }
383
384 pub fn verify(
385 &self,
386 attempt_ref: &DeviceJoinAttemptRef,
387 attempt: &DeviceJoinAttempt,
388 registration: &StoreDeviceRegistration,
389 initial_ack_ref: &StoreAckRef,
390 initial_ack: &StoreAck,
391 ) -> Result<(), StoreProtocolError> {
392 require_version(self.version)?;
393 if &self.attempt != attempt_ref
394 || attempt_ref.attempt_id != attempt.attempt_id
395 || attempt_ref.attempt_hash != attempt.attempt_hash()
396 || self.store_root_hash != registration.store_root.store_root_hash
397 || self.registration.device_id != registration.device_id
398 || self.bootstrap_cut != attempt.bootstrap_cut
399 {
400 return Err(StoreProtocolError::DeviceReadinessMismatch);
401 }
402 self.registration.verify_registration(registration)?;
403 if initial_ack.registration != self.registration
404 || initial_ack.sequence != 1
405 || initial_ack.successor.predecessor.is_some()
406 || initial_ack_ref != &self.initial_ack
407 || initial_ack_ref.registration != self.registration
408 || initial_ack_ref.sequence != initial_ack.sequence
409 || initial_ack_ref.ack_hash != initial_ack.ack_hash()
410 || initial_ack.store_cut != self.bootstrap_cut
411 {
412 return Err(StoreProtocolError::DeviceReadinessMismatch);
413 }
414 validate_store_history_cut(&self.bootstrap_cut)?;
415 if !keys::verify_signature_hex(
416 ®istration.device_signing_pubkey,
417 &self.signature,
418 &self.canonical_signed_bytes(),
419 ) {
420 return Err(StoreProtocolError::InvalidSignature);
421 }
422 Ok(())
423 }
424}
425
426#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
427#[serde(rename_all = "snake_case", deny_unknown_fields)]
428pub enum DeviceJoinOutcomeBody {
429 Activated { readiness: DeviceReadinessProof },
430 Cancelled,
431}
432
433#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
434#[serde(deny_unknown_fields)]
435pub struct DeviceJoinOutcome {
436 pub version: u32,
437 pub store_root_hash: ObjectHash,
438 pub attempt: DeviceJoinAttemptRef,
439 pub body: DeviceJoinOutcomeBody,
440 pub owner_registration: StoreDeviceRegistrationRef,
441 pub owner_grant: MembershipGrantId,
442 pub signature: String,
443}
444
445#[derive(Serialize)]
446struct DeviceJoinOutcomeSignedFields<'a> {
447 version: u32,
448 store_root_hash: ObjectHash,
449 attempt: &'a DeviceJoinAttemptRef,
450 body: &'a DeviceJoinOutcomeBody,
451 owner_registration: &'a StoreDeviceRegistrationRef,
452 owner_grant: &'a MembershipGrantId,
453}
454
455impl DeviceJoinOutcome {
456 pub fn signed(
457 attempt: DeviceJoinAttemptRef,
458 body: DeviceJoinOutcomeBody,
459 owner_registration: StoreDeviceRegistrationRef,
460 owner_grant: MembershipGrantId,
461 owner: &StoreDeviceRegistration,
462 owner_device_signer: &UserKeypair,
463 ) -> Result<Self, StoreProtocolError> {
464 owner_registration.verify_registration(owner)?;
465 if keys::public_key_hex(owner_device_signer) != owner.device_signing_pubkey {
466 return Err(StoreProtocolError::InvalidSignature);
467 }
468 let mut outcome = Self {
469 version: STORE_PROTOCOL_VERSION,
470 store_root_hash: owner.store_root.store_root_hash,
471 attempt,
472 body,
473 owner_registration,
474 owner_grant,
475 signature: String::new(),
476 };
477 let (_, signature) = keys::sign_hex(owner_device_signer, &outcome.canonical_signed_bytes());
478 outcome.signature = signature;
479 Ok(outcome)
480 }
481
482 pub(crate) fn canonical_signed_bytes(&self) -> Vec<u8> {
483 domain_json(
484 DEVICE_JOIN_OUTCOME_DOMAIN,
485 &DeviceJoinOutcomeSignedFields {
486 version: self.version,
487 store_root_hash: self.store_root_hash,
488 attempt: &self.attempt,
489 body: &self.body,
490 owner_registration: &self.owner_registration,
491 owner_grant: &self.owner_grant,
492 },
493 )
494 }
495
496 pub fn outcome_hash(&self) -> ObjectHash {
497 ObjectHash::digest(&self.canonical_signed_bytes())
498 }
499
500 pub fn to_bytes(&self) -> Vec<u8> {
501 serde_json::to_vec(self).expect("DeviceJoinOutcome serialization cannot fail")
502 }
503}
504
505#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
506#[serde(rename_all = "snake_case", deny_unknown_fields)]
507pub enum DeviceJoinOutcomeRef {
508 Activated {
509 attempt: DeviceJoinAttemptRef,
510 outcome_hash: ObjectHash,
511 object: ExactObjectRef,
512 },
513 Cancelled {
514 attempt: DeviceJoinAttemptRef,
515 outcome_hash: ObjectHash,
516 object: ExactObjectRef,
517 },
518}
519
520impl DeviceJoinOutcomeRef {
521 pub fn slot(&self) -> &ObjectSlot {
522 self.object().slot()
523 }
524
525 pub fn object(&self) -> &ExactObjectRef {
526 match self {
527 Self::Activated { object, .. } | Self::Cancelled { object, .. } => object,
528 }
529 }
530
531 pub fn attempt(&self) -> &DeviceJoinAttemptRef {
532 match self {
533 Self::Activated { attempt, .. } | Self::Cancelled { attempt, .. } => attempt,
534 }
535 }
536
537 pub fn verify_outcome(&self, outcome: &DeviceJoinOutcome) -> Result<(), StoreProtocolError> {
538 let (attempt, expected_hash, expects_activated) = match self {
539 Self::Activated {
540 attempt,
541 outcome_hash,
542 ..
543 } => (attempt, outcome_hash, true),
544 Self::Cancelled {
545 attempt,
546 outcome_hash,
547 ..
548 } => (attempt, outcome_hash, false),
549 };
550 if &outcome.attempt != attempt || outcome.outcome_hash() != *expected_hash {
551 return Err(StoreProtocolError::JoinOutcomeMismatch);
552 }
553 if expects_activated != matches!(outcome.body, DeviceJoinOutcomeBody::Activated { .. }) {
554 return Err(StoreProtocolError::JoinOutcomeMismatch);
555 }
556 Ok(())
557 }
558}
559
560#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
561#[serde(deny_unknown_fields)]
562pub struct OwnerRecoveryNodeRef {
563 pub owner_pubkey: String,
564 pub owner_grant: MembershipGrantId,
565 pub sequence: u64,
566 pub node_hash: ObjectHash,
567 pub object: ExactObjectRef,
568}
569
570#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
571#[serde(transparent)]
572pub struct OwnerRecoveryActivationId(ObjectHash);
573
574impl OwnerRecoveryActivationId {
575 pub fn derive(
576 root: &StoreRootRef,
577 owner_pubkey: &str,
578 owner_grant: &MembershipGrantId,
579 anchor: &GrantStreamAnchor,
580 ) -> Result<Self, StoreProtocolError> {
581 if !matches!(anchor, GrantStreamAnchor::OwnerRecovery { .. }) {
582 return Err(StoreProtocolError::OwnerRecoveryMismatch);
583 }
584 Ok(Self(ObjectHash::digest(&domain_json(
585 b"coven.owner-recovery-activation.v1\0",
586 &(root, owner_pubkey, owner_grant, anchor),
587 ))))
588 }
589}
590
591#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
592#[serde(rename_all = "snake_case", deny_unknown_fields)]
593pub enum OwnerRecoveryPosition {
594 BeforeFirst {
595 activation: OwnerRecoveryActivationId,
596 },
597 At {
598 node: OwnerRecoveryNodeRef,
599 },
600}
601
602#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
603#[serde(deny_unknown_fields)]
604pub struct OwnerRecoveryCursor {
605 pub owner_grant: MembershipGrantId,
606 pub position: OwnerRecoveryPosition,
607}