1use std::collections::BTreeSet;
4
5use serde::{Deserialize, Serialize};
6
7use crate::circle::{CircleBootstrapCoverageRef, CircleControlCoord, CircleId};
8use crate::circle_control::StoreMembershipStateRef;
9use crate::membership::MembershipGrantId;
10use crate::objects::ExactObjectRef;
11use crate::store_commit::{
12 CircleAckRef, CirclePackageRef, CircleSnapshotRef, ObjectHash, Signed, SignedBody,
13 SnapshotImageRef, StoreBatchCommitRef, StoreDeviceRegistration, StoreDeviceRegistrationRef,
14 StorePackageRef, StoreProtocolError,
15};
16use coven_keys::keys::{self, UserKeypair};
17
18const RECLAIM_EVIDENCE_DOMAIN: &[u8] = b"coven.store-reclaim-evidence.v1\0";
19const RECLAIM_AUTHORIZATION_DOMAIN: &[u8] = b"coven.store-reclaim-authorization.v1\0";
20const RECLAIM_RECEIPT_DOMAIN: &[u8] = b"coven.store-reclaim-receipt.v1\0";
21
22#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
27#[serde(rename_all = "snake_case", deny_unknown_fields)]
28pub enum ReclaimTarget {
29 StorePackage(StorePackageReclaimTarget),
30 CirclePackage(CirclePackageReclaimTarget),
31 CircleBootstrapImage(CircleBootstrapImageReclaimTarget),
32 CircleSnapshotImage(CircleSnapshotImageReclaimTarget),
33 AudienceBlob(AudienceBlobReclaimTarget),
34}
35
36impl ReclaimTarget {
37 pub fn object(&self) -> &ExactObjectRef {
38 match self {
39 Self::StorePackage(target) => &target.package.object,
40 Self::CirclePackage(target) => &target.package.package.object,
41 Self::CircleBootstrapImage(target) => &target.coverage.bootstrap.image.object,
42 Self::CircleSnapshotImage(target) => &target.image.object,
43 Self::AudienceBlob(target) => target.blob().object(),
44 }
45 }
46
47 pub fn activation(&self) -> ReclaimActivation<'_> {
48 match self {
49 Self::StorePackage(target) => ReclaimActivation::Commit(&target.activation),
50 Self::CirclePackage(target) => ReclaimActivation::Commit(&target.activation),
51 Self::CircleBootstrapImage(target) => {
52 ReclaimActivation::Commit(&target.coverage.activation_commit)
53 }
54 Self::CircleSnapshotImage(target) => {
55 ReclaimActivation::CircleSnapshotMetadata(CircleSnapshotStreamActivation {
56 circle_id: target.circle_id,
57 author_registration: &target.snapshot_author,
58 snapshot: &target.snapshot,
59 })
60 }
61 Self::AudienceBlob(AudienceBlobReclaimTarget::Store { blob }) => {
62 ReclaimActivation::StoreBlobInventory(blob)
63 }
64 Self::AudienceBlob(AudienceBlobReclaimTarget::Circle { source, .. }) => {
65 ReclaimActivation::PackageBlobBinding(source)
66 }
67 }
68 }
69}
70
71pub enum ReclaimActivation<'a> {
79 Commit(&'a StoreBatchCommitRef),
80 CircleSnapshotMetadata(CircleSnapshotStreamActivation<'a>),
81 PackageBlobBinding(&'a CirclePackageReclaimTarget),
82 StoreBlobInventory(&'a crate::blob::locator::StoredBlobRef),
83}
84
85impl ReclaimActivation<'_> {
86 pub fn names_authority_object(&self, object: &ExactObjectRef) -> bool {
87 match self {
88 Self::Commit(commit) => &commit.object == object,
89 Self::CircleSnapshotMetadata(source) => &source.snapshot.object == object,
90 Self::PackageBlobBinding(source) => &source.package.package.object == object,
91 Self::StoreBlobInventory(_) => false,
94 }
95 }
96}
97
98pub struct CircleSnapshotStreamActivation<'a> {
104 pub circle_id: CircleId,
105 pub author_registration: &'a StoreDeviceRegistrationRef,
106 pub snapshot: &'a CircleSnapshotRef,
107}
108
109#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
113#[serde(rename_all = "snake_case", deny_unknown_fields)]
114pub enum ReclaimClaim {
115 StorePackage(StorePackageReclaimClaim),
116 CirclePackage(CirclePackageReclaimClaim),
117 CircleBootstrapImage(CircleBootstrapImageReclaimClaim),
118 CircleSnapshotImage(CircleSnapshotImageReclaimClaim),
119 AudienceBlob(AudienceBlobReclaimClaim),
120}
121
122impl ReclaimClaim {
123 pub fn target(&self) -> ReclaimTarget {
124 match self {
125 Self::StorePackage(claim) => ReclaimTarget::StorePackage(claim.target.clone()),
126 Self::CirclePackage(claim) => ReclaimTarget::CirclePackage(claim.target().clone()),
127 Self::CircleBootstrapImage(claim) => {
128 ReclaimTarget::CircleBootstrapImage(claim.target.clone())
129 }
130 Self::CircleSnapshotImage(claim) => {
131 ReclaimTarget::CircleSnapshotImage(claim.target.clone())
132 }
133 Self::AudienceBlob(claim) => ReclaimTarget::AudienceBlob(claim.target.clone()),
134 }
135 }
136
137 fn validate(&self) -> Result<(), StoreProtocolError> {
138 match self {
139 Self::StorePackage(claim) => claim.validate(),
140 Self::CirclePackage(claim) => claim.validate(),
141 Self::CircleBootstrapImage(claim) => claim.validate(),
142 Self::CircleSnapshotImage(claim) => claim.validate(),
143 Self::AudienceBlob(claim) => claim.validate(),
144 }
145 }
146}
147
148#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
149#[serde(deny_unknown_fields)]
150pub struct StorePackageReclaimTarget {
151 pub package: StorePackageRef,
152 pub activation: StoreBatchCommitRef,
153}
154
155#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
156#[serde(deny_unknown_fields)]
157pub struct CirclePackageReclaimTarget {
158 pub package: CirclePackageRef,
159 pub activation: StoreBatchCommitRef,
160}
161
162#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
165#[serde(deny_unknown_fields)]
166pub struct CircleSnapshotLocator {
167 pub author_registration: StoreDeviceRegistrationRef,
168 pub circle_id: CircleId,
169 pub control: CircleControlCoord,
170 pub snapshot: CircleSnapshotRef,
171}
172
173#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
178#[serde(rename_all = "snake_case", deny_unknown_fields)]
179pub enum CirclePackageReclaimClaim {
180 SnapshotCovered(CirclePackageSnapshotCoverageClaim),
181 BeyondEpochCutoff(CirclePackageBeyondCutoffClaim),
182}
183
184impl CirclePackageReclaimClaim {
185 pub fn target(&self) -> &CirclePackageReclaimTarget {
186 match self {
187 Self::SnapshotCovered(claim) => &claim.target,
188 Self::BeyondEpochCutoff(claim) => &claim.target,
189 }
190 }
191
192 fn validate(&self) -> Result<(), StoreProtocolError> {
193 match self {
194 Self::SnapshotCovered(claim) => claim.validate(),
195 Self::BeyondEpochCutoff(claim) => claim.validate(),
196 }
197 }
198}
199
200#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
207#[serde(deny_unknown_fields)]
208pub struct CirclePackageBeyondCutoffClaim {
209 pub target: CirclePackageReclaimTarget,
210 pub successor_control: CircleControlCoord,
211}
212
213impl CirclePackageBeyondCutoffClaim {
214 fn validate(&self) -> Result<(), StoreProtocolError> {
215 self.successor_control.validate()?;
216 if self.successor_control == self.target.package.control {
217 return Err(StoreProtocolError::Malformed(
218 "Circle package beyond-cutoff successor is the package's own control".to_string(),
219 ));
220 }
221 if self.target.package.package.object == self.target.activation.object {
222 return Err(StoreProtocolError::Malformed(
223 "Circle package reclaim target aliases proof authority".to_string(),
224 ));
225 }
226 Ok(())
227 }
228}
229
230#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
236#[serde(deny_unknown_fields)]
237pub struct CirclePackageSnapshotCoverageClaim {
238 pub target: CirclePackageReclaimTarget,
239 pub covering_snapshot: CircleSnapshotLocator,
240 pub acknowledgements: Vec<CircleAckRef>,
241}
242
243impl CirclePackageSnapshotCoverageClaim {
244 fn validate(&self) -> Result<(), StoreProtocolError> {
245 if self.acknowledgements.is_empty() {
246 return Err(StoreProtocolError::Malformed(
247 "Circle package reclaim evidence has no acknowledgements".to_string(),
248 ));
249 }
250 if self
251 .acknowledgements
252 .windows(2)
253 .any(|pair| pair[0] >= pair[1])
254 {
255 return Err(StoreProtocolError::Malformed(
256 "Circle package reclaim acknowledgements are not strictly sorted and unique"
257 .to_string(),
258 ));
259 }
260 let circle_id = self.target.package.circle_id;
261 if self.covering_snapshot.circle_id != circle_id
262 || self.target.package.control != self.covering_snapshot.control
263 {
264 return Err(StoreProtocolError::Malformed(
265 "Circle package reclaim target, snapshot, and control name different Circles"
266 .to_string(),
267 ));
268 }
269 let mut registrations = BTreeSet::new();
270 if self.acknowledgements.iter().any(|acknowledgement| {
271 acknowledgement.circle_id != circle_id
272 || !registrations.insert(&acknowledgement.registration)
273 }) {
274 return Err(StoreProtocolError::Malformed(
275 "Circle package reclaim acknowledgement names another Circle or repeats a device"
276 .to_string(),
277 ));
278 }
279 let target_object = &self.target.package.package.object;
280 if *target_object == self.target.activation.object
281 || *target_object == self.covering_snapshot.snapshot.object
282 || self
283 .acknowledgements
284 .iter()
285 .any(|acknowledgement| acknowledgement.object == *target_object)
286 {
287 return Err(StoreProtocolError::Malformed(
288 "Circle package reclaim target aliases proof authority".to_string(),
289 ));
290 }
291 Ok(())
292 }
293}
294
295#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
301#[serde(deny_unknown_fields)]
302pub struct CircleBootstrapImageReclaimTarget {
303 pub coverage: CircleBootstrapCoverageRef,
304}
305
306#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
312#[serde(rename_all = "snake_case", deny_unknown_fields)]
313pub enum CircleBootstrapReclaimProof {
314 RecipientCoverage { acknowledgement: CircleAckRef },
318 LostAuthority {
321 acknowledgement: CircleAckRef,
322 successor_control: CircleControlCoord,
323 },
324}
325
326impl CircleBootstrapReclaimProof {
327 pub fn acknowledgement(&self) -> &CircleAckRef {
328 match self {
329 Self::RecipientCoverage { acknowledgement }
330 | Self::LostAuthority {
331 acknowledgement, ..
332 } => acknowledgement,
333 }
334 }
335}
336
337#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
340#[serde(deny_unknown_fields)]
341pub struct CircleBootstrapImageReclaimClaim {
342 pub target: CircleBootstrapImageReclaimTarget,
343 pub proof: CircleBootstrapReclaimProof,
344}
345
346impl CircleBootstrapImageReclaimClaim {
347 fn validate(&self) -> Result<(), StoreProtocolError> {
348 let circle_id = self.target.coverage.circle_id;
349 let acknowledgement = self.proof.acknowledgement();
350 if acknowledgement.circle_id != circle_id {
351 return Err(StoreProtocolError::Malformed(
352 "Circle bootstrap reclaim acknowledgement names another Circle".to_string(),
353 ));
354 }
355 let image = &self.target.coverage.bootstrap.image.object;
356 if *image == self.target.coverage.activation_commit.object
357 || *image == acknowledgement.object
358 {
359 return Err(StoreProtocolError::Malformed(
360 "Circle bootstrap reclaim target aliases proof authority".to_string(),
361 ));
362 }
363 if let CircleBootstrapReclaimProof::LostAuthority {
364 successor_control, ..
365 } = &self.proof
366 {
367 successor_control.validate()?;
368 if *successor_control == self.target.coverage.control {
369 return Err(StoreProtocolError::Malformed(
370 "Circle bootstrap lost-authority successor is the seed control".to_string(),
371 ));
372 }
373 }
374 Ok(())
375 }
376}
377
378#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
388#[serde(deny_unknown_fields)]
389pub struct CircleSnapshotImageReclaimTarget {
390 pub circle_id: CircleId,
391 pub snapshot_author: StoreDeviceRegistrationRef,
392 pub control: CircleControlCoord,
393 pub snapshot: CircleSnapshotRef,
394 pub image: SnapshotImageRef,
395}
396
397impl CircleSnapshotImageReclaimTarget {
398 pub fn snapshot_owner(
403 &self,
404 store_root_hash: ObjectHash,
405 ) -> Result<crate::remote_object::SnapshotObjectOwner, StoreProtocolError> {
406 Ok(crate::remote_object::SnapshotObjectOwner::Circle {
407 activation: crate::store_commit::circle_snapshot_stream_activation(
408 store_root_hash,
409 &self.snapshot_author,
410 self.circle_id,
411 )?,
412 generation: self.snapshot.generation,
413 })
414 }
415}
416
417#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
423#[serde(deny_unknown_fields)]
424pub struct CircleSnapshotImageReclaimClaim {
425 pub target: CircleSnapshotImageReclaimTarget,
426 pub superseding: CircleSnapshotRef,
427}
428
429impl CircleSnapshotImageReclaimClaim {
430 fn validate(&self) -> Result<(), StoreProtocolError> {
431 if self.superseding.generation <= self.target.snapshot.generation {
432 return Err(StoreProtocolError::Malformed(
433 "Circle snapshot reclaim names a superseding generation that is not later"
434 .to_string(),
435 ));
436 }
437 let image = &self.target.image.object;
438 if *image == self.target.snapshot.object || *image == self.superseding.object {
439 return Err(StoreProtocolError::Malformed(
440 "Circle snapshot reclaim target aliases proof authority".to_string(),
441 ));
442 }
443 Ok(())
444 }
445}
446
447#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
451#[serde(rename_all = "snake_case", deny_unknown_fields)]
452pub enum AudienceBlobBindingPackage {
453 Store(StorePackageRef),
454 Circle(CirclePackageRef),
455}
456
457impl AudienceBlobBindingPackage {
458 pub fn object(&self) -> &ExactObjectRef {
459 match self {
460 Self::Store(package) => &package.object,
461 Self::Circle(package) => &package.package.object,
462 }
463 }
464
465 pub fn remote_audience(&self) -> crate::blob::locator::RemoteAudience {
466 match self {
467 Self::Store(_) => crate::blob::locator::RemoteAudience::Store,
468 Self::Circle(package) => {
469 crate::blob::locator::RemoteAudience::Circle(package.circle_id)
470 }
471 }
472 }
473}
474
475#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
484#[serde(rename_all = "snake_case", deny_unknown_fields)]
485pub enum AudienceBlobReclaimTarget {
486 Store {
487 blob: crate::blob::locator::StoredBlobRef,
488 },
489 Circle {
490 blob: crate::blob::locator::StoredBlobRef,
491 source: CirclePackageReclaimTarget,
492 },
493}
494
495impl AudienceBlobReclaimTarget {
496 pub fn blob(&self) -> &crate::blob::locator::StoredBlobRef {
497 match self {
498 Self::Store { blob } | Self::Circle { blob, .. } => blob,
499 }
500 }
501}
502
503#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
508#[serde(deny_unknown_fields)]
509pub struct AudienceBlobReclaimClaim {
510 pub target: AudienceBlobReclaimTarget,
511}
512
513impl AudienceBlobReclaimClaim {
514 fn validate(&self) -> Result<(), StoreProtocolError> {
515 let audience = match &self.target {
516 AudienceBlobReclaimTarget::Store { .. } => crate::blob::locator::RemoteAudience::Store,
517 AudienceBlobReclaimTarget::Circle { blob, source } => {
518 if blob.object() == &source.package.package.object
519 || blob.object() == &source.activation.object
520 {
521 return Err(StoreProtocolError::Malformed(
522 "audience blob reclaim target aliases proof authority".to_string(),
523 ));
524 }
525 crate::blob::locator::RemoteAudience::Circle(source.package.circle_id)
526 }
527 };
528 if self.target.blob().locator().audience() != audience {
529 return Err(StoreProtocolError::Malformed(
530 "audience blob reclaim target names a package for another audience".to_string(),
531 ));
532 }
533 Ok(())
534 }
535}
536
537#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
538#[serde(deny_unknown_fields)]
539pub struct StorePackageReclaimClaim {
540 pub target: StorePackageReclaimTarget,
541}
542
543impl StorePackageReclaimClaim {
544 fn validate(&self) -> Result<(), StoreProtocolError> {
545 if self.target.package.object == self.target.activation.object {
546 return Err(StoreProtocolError::Malformed(
547 "Store package reclaim target aliases its activation".to_string(),
548 ));
549 }
550 Ok(())
551 }
552}
553
554#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
555#[serde(deny_unknown_fields)]
556pub struct ReclaimEvidenceRef {
557 pub evidence_hash: ObjectHash,
558 pub target: Box<ReclaimTarget>,
559 pub object: ExactObjectRef,
560}
561
562impl ReclaimEvidenceRef {
563 pub fn from_evidence(evidence: &ReclaimEvidence, object: ExactObjectRef) -> Self {
564 Self {
565 evidence_hash: evidence.evidence_hash(),
566 target: Box::new(evidence.claim.target()),
567 object,
568 }
569 }
570
571 pub fn verify(&self, evidence: &ReclaimEvidence) -> Result<(), StoreProtocolError> {
572 let actual = evidence.evidence_hash();
573 if actual != self.evidence_hash {
574 return Err(StoreProtocolError::ObjectHashMismatch {
575 expected: self.evidence_hash,
576 actual,
577 });
578 }
579 if evidence.claim.target() != *self.target {
580 return Err(StoreProtocolError::Malformed(
581 "reclaim target differs from its exact evidence reference".to_string(),
582 ));
583 }
584 evidence.verify()
585 }
586}
587
588#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
590#[serde(deny_unknown_fields)]
591pub struct ReclaimEvidenceBody {
592 pub store_root_hash: ObjectHash,
593 pub claim: ReclaimClaim,
594 pub author_pubkey: String,
595}
596
597impl SignedBody for ReclaimEvidenceBody {
598 const DOMAIN: &'static [u8] = RECLAIM_EVIDENCE_DOMAIN;
599}
600
601pub type ReclaimEvidence = Signed<ReclaimEvidenceBody>;
602
603impl ReclaimEvidence {
604 pub fn signed(
605 store_root_hash: ObjectHash,
606 claim: ReclaimClaim,
607 signer: &UserKeypair,
608 ) -> Result<Self, StoreProtocolError> {
609 claim.validate()?;
610 Ok(Signed::sign(
611 ReclaimEvidenceBody {
612 store_root_hash,
613 claim,
614 author_pubkey: keys::public_key_hex(signer),
615 },
616 signer,
617 ))
618 }
619
620 pub fn evidence_hash(&self) -> ObjectHash {
621 self.hash()
622 }
623
624 pub fn verify(&self) -> Result<(), StoreProtocolError> {
625 self.claim.validate()?;
626 let author_pubkey = self.author_pubkey.clone();
627 self.verify_by(&author_pubkey)
628 }
629}
630
631#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
632#[serde(deny_unknown_fields)]
633pub struct StoreReclaimAuthority {
634 pub membership: StoreMembershipStateRef,
635 pub owner_grant: MembershipGrantId,
636}
637
638#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
639#[serde(deny_unknown_fields)]
640pub struct ReclaimAuthorizationRef {
641 pub authorization_hash: ObjectHash,
642 pub evidence: ReclaimEvidenceRef,
643 pub object: ExactObjectRef,
644}
645
646impl ReclaimAuthorizationRef {
647 pub fn from_authorization(
648 authorization: &ReclaimAuthorization,
649 object: ExactObjectRef,
650 ) -> Self {
651 Self {
652 authorization_hash: authorization.authorization_hash(),
653 evidence: authorization.evidence.clone(),
654 object,
655 }
656 }
657
658 pub fn verify_identity(
659 &self,
660 authorization: &ReclaimAuthorization,
661 ) -> Result<(), StoreProtocolError> {
662 let actual = authorization.authorization_hash();
663 if actual != self.authorization_hash {
664 return Err(StoreProtocolError::ObjectHashMismatch {
665 expected: self.authorization_hash,
666 actual,
667 });
668 }
669 if authorization.evidence != self.evidence || authorization.target != *self.evidence.target
670 {
671 return Err(StoreProtocolError::Malformed(
672 "reclaim authorization target or evidence differs from its exact reference"
673 .to_string(),
674 ));
675 }
676 Ok(())
677 }
678
679 pub fn target(&self) -> &ReclaimTarget {
680 &self.evidence.target
681 }
682
683 pub fn target_activation(&self) -> ReclaimActivation<'_> {
684 self.evidence.target.activation()
685 }
686
687 pub fn verify(
688 &self,
689 authorization: &ReclaimAuthorization,
690 owner_pubkey: &str,
691 ) -> Result<(), StoreProtocolError> {
692 self.verify_identity(authorization)?;
693 authorization.verify(owner_pubkey)
694 }
695}
696
697#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
700#[serde(deny_unknown_fields)]
701pub struct ReclaimAuthorizationBody {
702 pub store_root_hash: ObjectHash,
703 pub target: ReclaimTarget,
704 pub evidence: ReclaimEvidenceRef,
705 pub authority: StoreReclaimAuthority,
706}
707
708impl SignedBody for ReclaimAuthorizationBody {
709 const DOMAIN: &'static [u8] = RECLAIM_AUTHORIZATION_DOMAIN;
710}
711
712pub type ReclaimAuthorization = Signed<ReclaimAuthorizationBody>;
713
714impl ReclaimAuthorization {
715 pub fn signed(
716 store_root_hash: ObjectHash,
717 target: ReclaimTarget,
718 evidence: ReclaimEvidenceRef,
719 authority: StoreReclaimAuthority,
720 signer: &UserKeypair,
721 ) -> Self {
722 Signed::sign(
723 ReclaimAuthorizationBody {
724 store_root_hash,
725 target,
726 evidence,
727 authority,
728 },
729 signer,
730 )
731 }
732
733 pub fn authorization_hash(&self) -> ObjectHash {
734 self.hash()
735 }
736
737 pub fn verify(&self, owner_pubkey: &str) -> Result<(), StoreProtocolError> {
738 self.verify_by(owner_pubkey)
739 }
740}
741
742#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
743#[serde(deny_unknown_fields)]
744pub struct ReclaimReceiptRef {
745 pub receipt_hash: ObjectHash,
746 pub authorization: ReclaimAuthorizationRef,
747 pub object: ExactObjectRef,
748}
749
750impl ReclaimReceiptRef {
751 pub fn from_receipt(receipt: &ReclaimReceipt, object: ExactObjectRef) -> Self {
752 Self {
753 receipt_hash: receipt.receipt_hash(),
754 authorization: receipt.authorization.clone(),
755 object,
756 }
757 }
758
759 pub fn verify_identity(&self, receipt: &ReclaimReceipt) -> Result<(), StoreProtocolError> {
760 let actual = receipt.receipt_hash();
761 if actual != self.receipt_hash {
762 return Err(StoreProtocolError::ObjectHashMismatch {
763 expected: self.receipt_hash,
764 actual,
765 });
766 }
767 if receipt.authorization != self.authorization {
768 return Err(StoreProtocolError::Malformed(
769 "reclaim receipt authorization differs from its exact reference".to_string(),
770 ));
771 }
772 Ok(())
773 }
774
775 pub fn verify(
776 &self,
777 receipt: &ReclaimReceipt,
778 executor: &StoreDeviceRegistration,
779 ) -> Result<(), StoreProtocolError> {
780 self.verify_identity(receipt)?;
781 receipt.verify(executor)
782 }
783}
784
785#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
788#[serde(deny_unknown_fields)]
789pub struct ReclaimReceiptBody {
790 pub store_root_hash: ObjectHash,
791 pub authorization: ReclaimAuthorizationRef,
792 pub provider_admin_state: StoreMembershipStateRef,
793 pub provider_admin_grant: crate::provider::ProviderAdminGrantId,
794 pub executor: StoreDeviceRegistrationRef,
795}
796
797impl SignedBody for ReclaimReceiptBody {
798 const DOMAIN: &'static [u8] = RECLAIM_RECEIPT_DOMAIN;
799}
800
801pub type ReclaimReceipt = Signed<ReclaimReceiptBody>;
802
803impl ReclaimReceipt {
804 #[allow(clippy::too_many_arguments)]
805 pub fn signed(
806 store_root_hash: ObjectHash,
807 authorization: ReclaimAuthorizationRef,
808 provider_admin_state: StoreMembershipStateRef,
809 provider_admin_grant: crate::provider::ProviderAdminGrantId,
810 executor: StoreDeviceRegistrationRef,
811 executor_registration: &StoreDeviceRegistration,
812 signer: &UserKeypair,
813 ) -> Result<Self, StoreProtocolError> {
814 executor.verify_registration(executor_registration)?;
815 crate::objects::verify_store_root(
816 store_root_hash,
817 executor_registration.store_root.store_root_hash,
818 )?;
819 if keys::public_key_hex(signer) != executor_registration.device_signing_pubkey {
820 return Err(StoreProtocolError::InvalidSignature);
821 }
822 Ok(Signed::sign(
823 ReclaimReceiptBody {
824 store_root_hash,
825 authorization,
826 provider_admin_state,
827 provider_admin_grant,
828 executor,
829 },
830 signer,
831 ))
832 }
833
834 pub fn receipt_hash(&self) -> ObjectHash {
835 self.hash()
836 }
837
838 pub fn verify(&self, executor: &StoreDeviceRegistration) -> Result<(), StoreProtocolError> {
839 self.executor.verify_registration(executor)?;
840 crate::objects::verify_store_root(
841 self.store_root_hash,
842 executor.store_root.store_root_hash,
843 )?;
844 self.verify_by(&executor.device_signing_pubkey)
845 }
846}
847
848pub fn reclaim_evidence_semantic_prefix(evidence_hash: ObjectHash) -> String {
849 format!("store-v1/reclaim/evidence/{evidence_hash}")
850}
851
852pub fn reclaim_authorization_semantic_prefix(authorization_hash: ObjectHash) -> String {
853 format!("store-v1/reclaim/authorizations/{authorization_hash}")
854}
855
856pub fn reclaim_receipt_semantic_prefix(receipt_hash: ObjectHash) -> String {
857 format!("store-v1/reclaim/receipts/{receipt_hash}")
858}
859
860#[cfg(test)]
861mod tests;