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, MembershipRollupRef, ObjectHash, Signed,
13 SignedBody, SnapshotImageRef, StoreAckRef, StoreBatchCommitRef, StoreDeviceRegistration,
14 StoreDeviceRegistrationRef, StorePackageRef, StoreProtocolError, StoreSnapshotLocator,
15 StoreSnapshotRef, StreamActivationId,
16};
17use coven_keys::keys::{self, UserKeypair};
18
19const RECLAIM_EVIDENCE_DOMAIN: &[u8] = b"coven.store-reclaim-evidence.v1\0";
20const RECLAIM_AUTHORIZATION_DOMAIN: &[u8] = b"coven.store-reclaim-authorization.v1\0";
21const RECLAIM_RECEIPT_DOMAIN: &[u8] = b"coven.store-reclaim-receipt.v1\0";
22
23#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
28#[serde(rename_all = "snake_case", deny_unknown_fields)]
29pub enum ReclaimTarget {
30 StorePackage(StorePackageReclaimTarget),
31 CirclePackage(CirclePackageReclaimTarget),
32 CircleBootstrapImage(CircleBootstrapImageReclaimTarget),
33 CircleSnapshotImage(CircleSnapshotImageReclaimTarget),
34 StoreMembershipRollup(StoreMembershipRollupReclaimTarget),
35 AudienceBlob(AudienceBlobReclaimTarget),
36}
37
38impl ReclaimTarget {
39 pub fn object(&self) -> &ExactObjectRef {
40 match self {
41 Self::StorePackage(target) => &target.package.object,
42 Self::CirclePackage(target) => &target.package.package.object,
43 Self::CircleBootstrapImage(target) => &target.coverage.bootstrap.image.object,
44 Self::CircleSnapshotImage(target) => &target.image.object,
45 Self::StoreMembershipRollup(target) => &target.rollup.object,
46 Self::AudienceBlob(target) => target.blob.object(),
47 }
48 }
49
50 pub fn activation(&self) -> ReclaimActivation<'_> {
51 match self {
52 Self::StorePackage(target) => ReclaimActivation::Commit(&target.activation),
53 Self::CirclePackage(target) => ReclaimActivation::Commit(&target.activation),
54 Self::CircleBootstrapImage(target) => {
55 ReclaimActivation::Commit(&target.coverage.activation_commit)
56 }
57 Self::CircleSnapshotImage(target) => {
58 ReclaimActivation::CircleSnapshotMetadata(CircleSnapshotStreamActivation {
59 circle_id: target.circle_id,
60 author_registration: &target.snapshot_author,
61 snapshot: &target.snapshot,
62 })
63 }
64 Self::StoreMembershipRollup(target) => {
65 ReclaimActivation::StoreSnapshotMetadata(StoreSnapshotStreamActivation {
66 author_registration: &target.snapshot_author,
67 snapshot: &target.snapshot,
68 })
69 }
70 Self::AudienceBlob(target) => {
71 ReclaimActivation::PackageBlobBinding(PackageBlobBindingActivation {
72 package: &target.package,
73 activation: &target.activation,
74 })
75 }
76 }
77 }
78}
79
80pub enum ReclaimActivation<'a> {
88 Commit(&'a StoreBatchCommitRef),
89 CircleSnapshotMetadata(CircleSnapshotStreamActivation<'a>),
90 StoreSnapshotMetadata(StoreSnapshotStreamActivation<'a>),
91 PackageBlobBinding(PackageBlobBindingActivation<'a>),
92}
93
94impl ReclaimActivation<'_> {
95 pub fn object(&self) -> &ExactObjectRef {
98 match self {
99 Self::Commit(commit) => &commit.object,
100 Self::CircleSnapshotMetadata(activation) => &activation.snapshot.object,
101 Self::StoreSnapshotMetadata(activation) => &activation.snapshot.object,
102 Self::PackageBlobBinding(activation) => activation.package.object(),
103 }
104 }
105}
106
107pub struct PackageBlobBindingActivation<'a> {
113 pub package: &'a AudienceBlobBindingPackage,
114 pub activation: &'a StoreBatchCommitRef,
115}
116
117pub struct CircleSnapshotStreamActivation<'a> {
123 pub circle_id: CircleId,
124 pub author_registration: &'a StoreDeviceRegistrationRef,
125 pub snapshot: &'a CircleSnapshotRef,
126}
127
128pub struct StoreSnapshotStreamActivation<'a> {
133 pub author_registration: &'a StoreDeviceRegistrationRef,
134 pub snapshot: &'a StoreSnapshotRef,
135}
136
137#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
141#[serde(rename_all = "snake_case", deny_unknown_fields)]
142pub enum ReclaimClaim {
143 StorePackage(StorePackageReclaimClaim),
144 CirclePackage(CirclePackageReclaimClaim),
145 CircleBootstrapImage(CircleBootstrapImageReclaimClaim),
146 CircleSnapshotImage(CircleSnapshotImageReclaimClaim),
147 StoreMembershipRollup(StoreMembershipRollupReclaimClaim),
148 AudienceBlob(AudienceBlobReclaimClaim),
149}
150
151impl ReclaimClaim {
152 pub fn target(&self) -> ReclaimTarget {
153 match self {
154 Self::StorePackage(claim) => ReclaimTarget::StorePackage(claim.target.clone()),
155 Self::CirclePackage(claim) => ReclaimTarget::CirclePackage(claim.target().clone()),
156 Self::CircleBootstrapImage(claim) => {
157 ReclaimTarget::CircleBootstrapImage(claim.target.clone())
158 }
159 Self::CircleSnapshotImage(claim) => {
160 ReclaimTarget::CircleSnapshotImage(claim.target.clone())
161 }
162 Self::StoreMembershipRollup(claim) => {
163 ReclaimTarget::StoreMembershipRollup(claim.target.clone())
164 }
165 Self::AudienceBlob(claim) => ReclaimTarget::AudienceBlob(claim.target.clone()),
166 }
167 }
168
169 fn validate(&self) -> Result<(), StoreProtocolError> {
170 match self {
171 Self::StorePackage(claim) => claim.validate(),
172 Self::CirclePackage(claim) => claim.validate(),
173 Self::CircleBootstrapImage(claim) => claim.validate(),
174 Self::CircleSnapshotImage(claim) => claim.validate(),
175 Self::StoreMembershipRollup(claim) => claim.validate(),
176 Self::AudienceBlob(claim) => claim.validate(),
177 }
178 }
179}
180
181#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
182#[serde(deny_unknown_fields)]
183pub struct StorePackageReclaimTarget {
184 pub package: StorePackageRef,
185 pub activation: StoreBatchCommitRef,
186}
187
188#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
189#[serde(deny_unknown_fields)]
190pub struct CirclePackageReclaimTarget {
191 pub package: CirclePackageRef,
192 pub activation: StoreBatchCommitRef,
193}
194
195#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
198#[serde(deny_unknown_fields)]
199pub struct CircleSnapshotLocator {
200 pub author_registration: StoreDeviceRegistrationRef,
201 pub circle_id: CircleId,
202 pub control: CircleControlCoord,
203 pub snapshot: CircleSnapshotRef,
204}
205
206#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
211#[serde(rename_all = "snake_case", deny_unknown_fields)]
212pub enum CirclePackageReclaimClaim {
213 SnapshotCovered(CirclePackageSnapshotCoverageClaim),
214 BeyondEpochCutoff(CirclePackageBeyondCutoffClaim),
215}
216
217impl CirclePackageReclaimClaim {
218 pub fn target(&self) -> &CirclePackageReclaimTarget {
219 match self {
220 Self::SnapshotCovered(claim) => &claim.target,
221 Self::BeyondEpochCutoff(claim) => &claim.target,
222 }
223 }
224
225 fn validate(&self) -> Result<(), StoreProtocolError> {
226 match self {
227 Self::SnapshotCovered(claim) => claim.validate(),
228 Self::BeyondEpochCutoff(claim) => claim.validate(),
229 }
230 }
231}
232
233#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
240#[serde(deny_unknown_fields)]
241pub struct CirclePackageBeyondCutoffClaim {
242 pub target: CirclePackageReclaimTarget,
243 pub successor_control: CircleControlCoord,
244}
245
246impl CirclePackageBeyondCutoffClaim {
247 fn validate(&self) -> Result<(), StoreProtocolError> {
248 self.successor_control.validate()?;
249 if self.successor_control == self.target.package.control {
250 return Err(StoreProtocolError::Malformed(
251 "Circle package beyond-cutoff successor is the package's own control".to_string(),
252 ));
253 }
254 if self.target.package.package.object == self.target.activation.object {
255 return Err(StoreProtocolError::Malformed(
256 "Circle package reclaim target aliases proof authority".to_string(),
257 ));
258 }
259 Ok(())
260 }
261}
262
263#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
269#[serde(deny_unknown_fields)]
270pub struct CirclePackageSnapshotCoverageClaim {
271 pub target: CirclePackageReclaimTarget,
272 pub covering_snapshot: CircleSnapshotLocator,
273 pub acknowledgements: Vec<CircleAckRef>,
274}
275
276impl CirclePackageSnapshotCoverageClaim {
277 fn validate(&self) -> Result<(), StoreProtocolError> {
278 if self.acknowledgements.is_empty() {
279 return Err(StoreProtocolError::Malformed(
280 "Circle package reclaim evidence has no acknowledgements".to_string(),
281 ));
282 }
283 if self
284 .acknowledgements
285 .windows(2)
286 .any(|pair| pair[0] >= pair[1])
287 {
288 return Err(StoreProtocolError::Malformed(
289 "Circle package reclaim acknowledgements are not strictly sorted and unique"
290 .to_string(),
291 ));
292 }
293 let circle_id = self.target.package.circle_id;
294 if self.covering_snapshot.circle_id != circle_id
295 || self.target.package.control != self.covering_snapshot.control
296 {
297 return Err(StoreProtocolError::Malformed(
298 "Circle package reclaim target, snapshot, and control name different Circles"
299 .to_string(),
300 ));
301 }
302 let mut registrations = BTreeSet::new();
303 if self.acknowledgements.iter().any(|acknowledgement| {
304 acknowledgement.circle_id != circle_id
305 || !registrations.insert(&acknowledgement.registration)
306 }) {
307 return Err(StoreProtocolError::Malformed(
308 "Circle package reclaim acknowledgement names another Circle or repeats a device"
309 .to_string(),
310 ));
311 }
312 let target_object = &self.target.package.package.object;
313 if *target_object == self.target.activation.object
314 || *target_object == self.covering_snapshot.snapshot.object
315 || self
316 .acknowledgements
317 .iter()
318 .any(|acknowledgement| acknowledgement.object == *target_object)
319 {
320 return Err(StoreProtocolError::Malformed(
321 "Circle package reclaim target aliases proof authority".to_string(),
322 ));
323 }
324 Ok(())
325 }
326}
327
328#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
334#[serde(deny_unknown_fields)]
335pub struct CircleBootstrapImageReclaimTarget {
336 pub coverage: CircleBootstrapCoverageRef,
337}
338
339#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
345#[serde(rename_all = "snake_case", deny_unknown_fields)]
346pub enum CircleBootstrapReclaimProof {
347 RecipientCoverage { acknowledgement: CircleAckRef },
351 LostAuthority {
354 acknowledgement: CircleAckRef,
355 successor_control: CircleControlCoord,
356 },
357}
358
359impl CircleBootstrapReclaimProof {
360 pub fn acknowledgement(&self) -> &CircleAckRef {
361 match self {
362 Self::RecipientCoverage { acknowledgement }
363 | Self::LostAuthority {
364 acknowledgement, ..
365 } => acknowledgement,
366 }
367 }
368}
369
370#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
373#[serde(deny_unknown_fields)]
374pub struct CircleBootstrapImageReclaimClaim {
375 pub target: CircleBootstrapImageReclaimTarget,
376 pub proof: CircleBootstrapReclaimProof,
377}
378
379impl CircleBootstrapImageReclaimClaim {
380 fn validate(&self) -> Result<(), StoreProtocolError> {
381 let circle_id = self.target.coverage.circle_id;
382 let acknowledgement = self.proof.acknowledgement();
383 if acknowledgement.circle_id != circle_id {
384 return Err(StoreProtocolError::Malformed(
385 "Circle bootstrap reclaim acknowledgement names another Circle".to_string(),
386 ));
387 }
388 let image = &self.target.coverage.bootstrap.image.object;
389 if *image == self.target.coverage.activation_commit.object
390 || *image == acknowledgement.object
391 {
392 return Err(StoreProtocolError::Malformed(
393 "Circle bootstrap reclaim target aliases proof authority".to_string(),
394 ));
395 }
396 if let CircleBootstrapReclaimProof::LostAuthority {
397 successor_control, ..
398 } = &self.proof
399 {
400 successor_control.validate()?;
401 if *successor_control == self.target.coverage.control {
402 return Err(StoreProtocolError::Malformed(
403 "Circle bootstrap lost-authority successor is the seed control".to_string(),
404 ));
405 }
406 }
407 Ok(())
408 }
409}
410
411#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
421#[serde(deny_unknown_fields)]
422pub struct CircleSnapshotImageReclaimTarget {
423 pub circle_id: CircleId,
424 pub snapshot_author: StoreDeviceRegistrationRef,
425 pub control: CircleControlCoord,
426 pub snapshot: CircleSnapshotRef,
427 pub image: SnapshotImageRef,
428}
429
430impl CircleSnapshotImageReclaimTarget {
431 pub fn snapshot_owner(
436 &self,
437 store_root_hash: ObjectHash,
438 ) -> Result<crate::remote_object::SnapshotObjectOwner, StoreProtocolError> {
439 Ok(crate::remote_object::SnapshotObjectOwner {
440 activation: crate::store_commit::circle_snapshot_stream_activation(
441 store_root_hash,
442 &self.snapshot_author,
443 self.circle_id,
444 &self.snapshot_author.device_id.to_string(),
445 )?,
446 generation: self.snapshot.generation,
447 })
448 }
449}
450
451#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
457#[serde(deny_unknown_fields)]
458pub struct CircleSnapshotImageReclaimClaim {
459 pub target: CircleSnapshotImageReclaimTarget,
460 pub superseding: CircleSnapshotRef,
461}
462
463impl CircleSnapshotImageReclaimClaim {
464 fn validate(&self) -> Result<(), StoreProtocolError> {
465 if self.superseding.generation <= self.target.snapshot.generation {
466 return Err(StoreProtocolError::Malformed(
467 "Circle snapshot reclaim names a superseding generation that is not later"
468 .to_string(),
469 ));
470 }
471 let image = &self.target.image.object;
472 if *image == self.target.snapshot.object || *image == self.superseding.object {
473 return Err(StoreProtocolError::Malformed(
474 "Circle snapshot reclaim target aliases proof authority".to_string(),
475 ));
476 }
477 Ok(())
478 }
479}
480
481#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
490#[serde(deny_unknown_fields)]
491pub struct StoreMembershipRollupReclaimTarget {
492 pub snapshot_author: StoreDeviceRegistrationRef,
493 pub activation: StreamActivationId,
494 pub snapshot: StoreSnapshotRef,
495 pub rollup: MembershipRollupRef,
496}
497
498impl StoreMembershipRollupReclaimTarget {
499 pub fn snapshot_owner(&self) -> crate::remote_object::SnapshotObjectOwner {
502 crate::remote_object::SnapshotObjectOwner {
503 activation: self.activation,
504 generation: self.snapshot.generation,
505 }
506 }
507}
508
509#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
512#[serde(deny_unknown_fields)]
513pub struct StoreMembershipRollupReclaimClaim {
514 pub target: StoreMembershipRollupReclaimTarget,
515 pub superseding: StoreSnapshotRef,
516}
517
518impl StoreMembershipRollupReclaimClaim {
519 fn validate(&self) -> Result<(), StoreProtocolError> {
520 if self.superseding.generation <= self.target.snapshot.generation {
521 return Err(StoreProtocolError::Malformed(
522 "Store membership rollup reclaim names a superseding generation that is not later"
523 .to_string(),
524 ));
525 }
526 let rollup = &self.target.rollup.object;
527 if *rollup == self.target.snapshot.object || *rollup == self.superseding.object {
528 return Err(StoreProtocolError::Malformed(
529 "Store membership rollup reclaim target aliases proof authority".to_string(),
530 ));
531 }
532 Ok(())
533 }
534}
535
536#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
540#[serde(rename_all = "snake_case", deny_unknown_fields)]
541pub enum AudienceBlobBindingPackage {
542 Store(StorePackageRef),
543 Circle(CirclePackageRef),
544}
545
546impl AudienceBlobBindingPackage {
547 pub fn object(&self) -> &ExactObjectRef {
548 match self {
549 Self::Store(package) => &package.object,
550 Self::Circle(package) => &package.package.object,
551 }
552 }
553
554 pub fn remote_audience(&self) -> crate::blob::locator::RemoteAudience {
555 match self {
556 Self::Store(_) => crate::blob::locator::RemoteAudience::Store,
557 Self::Circle(package) => {
558 crate::blob::locator::RemoteAudience::Circle(package.circle_id)
559 }
560 }
561 }
562}
563
564#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
573#[serde(deny_unknown_fields)]
574pub struct AudienceBlobReclaimTarget {
575 pub blob: crate::blob::locator::StoredBlobRef,
576 pub package: AudienceBlobBindingPackage,
577 pub activation: StoreBatchCommitRef,
578}
579
580#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
585#[serde(deny_unknown_fields)]
586pub struct AudienceBlobReclaimClaim {
587 pub target: AudienceBlobReclaimTarget,
588}
589
590impl AudienceBlobReclaimClaim {
591 fn validate(&self) -> Result<(), StoreProtocolError> {
592 let blob = self.target.blob.object();
593 if blob == self.target.package.object() || *blob == self.target.activation.object {
594 return Err(StoreProtocolError::Malformed(
595 "audience blob reclaim target aliases proof authority".to_string(),
596 ));
597 }
598 if self.target.blob.locator().audience() != self.target.package.remote_audience() {
599 return Err(StoreProtocolError::Malformed(
600 "audience blob reclaim target names a package for another audience".to_string(),
601 ));
602 }
603 Ok(())
604 }
605}
606
607#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
608#[serde(deny_unknown_fields)]
609pub struct StorePackageReclaimClaim {
610 pub target: StorePackageReclaimTarget,
611 pub covering_snapshot: StoreSnapshotLocator,
612 pub acknowledgements: Vec<StoreAckRef>,
613}
614
615impl StorePackageReclaimClaim {
616 fn validate(&self) -> Result<(), StoreProtocolError> {
617 if self.acknowledgements.is_empty() {
618 return Err(StoreProtocolError::Malformed(
619 "Store package reclaim evidence has no acknowledgements".to_string(),
620 ));
621 }
622 if self
623 .acknowledgements
624 .windows(2)
625 .any(|pair| pair[0] >= pair[1])
626 {
627 return Err(StoreProtocolError::Malformed(
628 "Store package reclaim acknowledgements are not strictly sorted and unique"
629 .to_string(),
630 ));
631 }
632 let mut registrations = BTreeSet::new();
633 if self
634 .acknowledgements
635 .iter()
636 .any(|acknowledgement| !registrations.insert(&acknowledgement.registration))
637 {
638 return Err(StoreProtocolError::Malformed(
639 "Store package reclaim evidence repeats a device registration".to_string(),
640 ));
641 }
642 if self.target.package.object == self.target.activation.object
643 || self.target.package.object == self.covering_snapshot.snapshot.object
644 || self
645 .acknowledgements
646 .iter()
647 .any(|acknowledgement| acknowledgement.object == self.target.package.object)
648 {
649 return Err(StoreProtocolError::Malformed(
650 "Store package reclaim target aliases proof authority".to_string(),
651 ));
652 }
653 Ok(())
654 }
655}
656
657#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
658#[serde(deny_unknown_fields)]
659pub struct ReclaimEvidenceRef {
660 pub evidence_hash: ObjectHash,
661 pub target: Box<ReclaimTarget>,
662 pub object: ExactObjectRef,
663}
664
665impl ReclaimEvidenceRef {
666 pub fn from_evidence(evidence: &ReclaimEvidence, object: ExactObjectRef) -> Self {
667 Self {
668 evidence_hash: evidence.evidence_hash(),
669 target: Box::new(evidence.claim.target()),
670 object,
671 }
672 }
673
674 pub fn verify(&self, evidence: &ReclaimEvidence) -> Result<(), StoreProtocolError> {
675 let actual = evidence.evidence_hash();
676 if actual != self.evidence_hash {
677 return Err(StoreProtocolError::ObjectHashMismatch {
678 expected: self.evidence_hash,
679 actual,
680 });
681 }
682 if evidence.claim.target() != *self.target {
683 return Err(StoreProtocolError::Malformed(
684 "reclaim target differs from its exact evidence reference".to_string(),
685 ));
686 }
687 evidence.verify()
688 }
689}
690
691#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
693#[serde(deny_unknown_fields)]
694pub struct ReclaimEvidenceBody {
695 pub store_root_hash: ObjectHash,
696 pub claim: ReclaimClaim,
697 pub author_pubkey: String,
698}
699
700impl SignedBody for ReclaimEvidenceBody {
701 const DOMAIN: &'static [u8] = RECLAIM_EVIDENCE_DOMAIN;
702}
703
704pub type ReclaimEvidence = Signed<ReclaimEvidenceBody>;
705
706impl ReclaimEvidence {
707 pub fn signed(
708 store_root_hash: ObjectHash,
709 claim: ReclaimClaim,
710 signer: &UserKeypair,
711 ) -> Result<Self, StoreProtocolError> {
712 claim.validate()?;
713 Ok(Signed::sign(
714 ReclaimEvidenceBody {
715 store_root_hash,
716 claim,
717 author_pubkey: keys::public_key_hex(signer),
718 },
719 signer,
720 ))
721 }
722
723 pub fn evidence_hash(&self) -> ObjectHash {
724 self.hash()
725 }
726
727 pub fn verify(&self) -> Result<(), StoreProtocolError> {
728 self.claim.validate()?;
729 let author_pubkey = self.author_pubkey.clone();
730 self.verify_by(&author_pubkey)
731 }
732}
733
734#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
735#[serde(deny_unknown_fields)]
736pub struct StoreReclaimAuthority {
737 pub membership: StoreMembershipStateRef,
738 pub owner_grant: MembershipGrantId,
739}
740
741#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
742#[serde(deny_unknown_fields)]
743pub struct ReclaimAuthorizationRef {
744 pub authorization_hash: ObjectHash,
745 pub evidence: ReclaimEvidenceRef,
746 pub object: ExactObjectRef,
747}
748
749impl ReclaimAuthorizationRef {
750 pub fn from_authorization(
751 authorization: &ReclaimAuthorization,
752 object: ExactObjectRef,
753 ) -> Self {
754 Self {
755 authorization_hash: authorization.authorization_hash(),
756 evidence: authorization.evidence.clone(),
757 object,
758 }
759 }
760
761 pub fn verify_identity(
762 &self,
763 authorization: &ReclaimAuthorization,
764 ) -> Result<(), StoreProtocolError> {
765 let actual = authorization.authorization_hash();
766 if actual != self.authorization_hash {
767 return Err(StoreProtocolError::ObjectHashMismatch {
768 expected: self.authorization_hash,
769 actual,
770 });
771 }
772 if authorization.evidence != self.evidence || authorization.target != *self.evidence.target
773 {
774 return Err(StoreProtocolError::Malformed(
775 "reclaim authorization target or evidence differs from its exact reference"
776 .to_string(),
777 ));
778 }
779 Ok(())
780 }
781
782 pub fn target(&self) -> &ReclaimTarget {
783 &self.evidence.target
784 }
785
786 pub fn target_activation(&self) -> ReclaimActivation<'_> {
787 self.evidence.target.activation()
788 }
789
790 pub fn verify(
791 &self,
792 authorization: &ReclaimAuthorization,
793 owner_pubkey: &str,
794 ) -> Result<(), StoreProtocolError> {
795 self.verify_identity(authorization)?;
796 authorization.verify(owner_pubkey)
797 }
798}
799
800#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
803#[serde(deny_unknown_fields)]
804pub struct ReclaimAuthorizationBody {
805 pub store_root_hash: ObjectHash,
806 pub target: ReclaimTarget,
807 pub evidence: ReclaimEvidenceRef,
808 pub authority: StoreReclaimAuthority,
809}
810
811impl SignedBody for ReclaimAuthorizationBody {
812 const DOMAIN: &'static [u8] = RECLAIM_AUTHORIZATION_DOMAIN;
813}
814
815pub type ReclaimAuthorization = Signed<ReclaimAuthorizationBody>;
816
817impl ReclaimAuthorization {
818 pub fn signed(
819 store_root_hash: ObjectHash,
820 target: ReclaimTarget,
821 evidence: ReclaimEvidenceRef,
822 authority: StoreReclaimAuthority,
823 signer: &UserKeypair,
824 ) -> Self {
825 Signed::sign(
826 ReclaimAuthorizationBody {
827 store_root_hash,
828 target,
829 evidence,
830 authority,
831 },
832 signer,
833 )
834 }
835
836 pub fn authorization_hash(&self) -> ObjectHash {
837 self.hash()
838 }
839
840 pub fn verify(&self, owner_pubkey: &str) -> Result<(), StoreProtocolError> {
841 self.verify_by(owner_pubkey)
842 }
843}
844
845#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
846#[serde(deny_unknown_fields)]
847pub struct ReclaimReceiptRef {
848 pub receipt_hash: ObjectHash,
849 pub authorization: ReclaimAuthorizationRef,
850 pub object: ExactObjectRef,
851}
852
853impl ReclaimReceiptRef {
854 pub fn from_receipt(receipt: &ReclaimReceipt, object: ExactObjectRef) -> Self {
855 Self {
856 receipt_hash: receipt.receipt_hash(),
857 authorization: receipt.authorization.clone(),
858 object,
859 }
860 }
861
862 pub fn verify_identity(&self, receipt: &ReclaimReceipt) -> Result<(), StoreProtocolError> {
863 let actual = receipt.receipt_hash();
864 if actual != self.receipt_hash {
865 return Err(StoreProtocolError::ObjectHashMismatch {
866 expected: self.receipt_hash,
867 actual,
868 });
869 }
870 if receipt.authorization != self.authorization {
871 return Err(StoreProtocolError::Malformed(
872 "reclaim receipt authorization differs from its exact reference".to_string(),
873 ));
874 }
875 Ok(())
876 }
877
878 pub fn verify(
879 &self,
880 receipt: &ReclaimReceipt,
881 executor: &StoreDeviceRegistration,
882 ) -> Result<(), StoreProtocolError> {
883 self.verify_identity(receipt)?;
884 receipt.verify(executor)
885 }
886}
887
888#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
891#[serde(deny_unknown_fields)]
892pub struct ReclaimReceiptBody {
893 pub store_root_hash: ObjectHash,
894 pub authorization: ReclaimAuthorizationRef,
895 pub provider_admin_state: StoreMembershipStateRef,
896 pub provider_admin_grant: crate::provider::ProviderAdminGrantId,
897 pub executor: StoreDeviceRegistrationRef,
898}
899
900impl SignedBody for ReclaimReceiptBody {
901 const DOMAIN: &'static [u8] = RECLAIM_RECEIPT_DOMAIN;
902}
903
904pub type ReclaimReceipt = Signed<ReclaimReceiptBody>;
905
906impl ReclaimReceipt {
907 #[allow(clippy::too_many_arguments)]
908 pub fn signed(
909 store_root_hash: ObjectHash,
910 authorization: ReclaimAuthorizationRef,
911 provider_admin_state: StoreMembershipStateRef,
912 provider_admin_grant: crate::provider::ProviderAdminGrantId,
913 executor: StoreDeviceRegistrationRef,
914 executor_registration: &StoreDeviceRegistration,
915 signer: &UserKeypair,
916 ) -> Result<Self, StoreProtocolError> {
917 executor.verify_registration(executor_registration)?;
918 crate::objects::verify_store_root(
919 store_root_hash,
920 executor_registration.store_root.store_root_hash,
921 )?;
922 if keys::public_key_hex(signer) != executor_registration.device_signing_pubkey {
923 return Err(StoreProtocolError::InvalidSignature);
924 }
925 Ok(Signed::sign(
926 ReclaimReceiptBody {
927 store_root_hash,
928 authorization,
929 provider_admin_state,
930 provider_admin_grant,
931 executor,
932 },
933 signer,
934 ))
935 }
936
937 pub fn receipt_hash(&self) -> ObjectHash {
938 self.hash()
939 }
940
941 pub fn verify(&self, executor: &StoreDeviceRegistration) -> Result<(), StoreProtocolError> {
942 self.executor.verify_registration(executor)?;
943 crate::objects::verify_store_root(
944 self.store_root_hash,
945 executor.store_root.store_root_hash,
946 )?;
947 self.verify_by(&executor.device_signing_pubkey)
948 }
949}
950
951pub fn reclaim_evidence_semantic_prefix(evidence_hash: ObjectHash) -> String {
952 format!("store-v1/reclaim/evidence/{evidence_hash}")
953}
954
955pub fn reclaim_authorization_semantic_prefix(authorization_hash: ObjectHash) -> String {
956 format!("store-v1/reclaim/authorizations/{authorization_hash}")
957}
958
959pub fn reclaim_receipt_semantic_prefix(receipt_hash: ObjectHash) -> String {
960 format!("store-v1/reclaim/receipts/{receipt_hash}")
961}
962
963#[cfg(test)]
964mod tests;