Skip to main content

coven_protocol/store_commit/device_state/
exclusion.rs

1use super::*;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
4#[serde(transparent)]
5pub struct StoreDeviceExclusionProposalId(ObjectHash);
6
7impl StoreDeviceExclusionProposalId {
8    pub fn from_hash(hash: ObjectHash) -> Self {
9        Self(hash)
10    }
11}
12
13impl fmt::Display for StoreDeviceExclusionProposalId {
14    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
15        fmt::Display::fmt(&self.0, formatter)
16    }
17}
18
19#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
20#[serde(deny_unknown_fields)]
21pub struct StoreDeviceExclusionProposalRef {
22    pub proposal_id: StoreDeviceExclusionProposalId,
23    pub target: StoreDeviceRegistrationRef,
24    pub proposal_hash: ObjectHash,
25    pub object: ExactObjectRef,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
29#[serde(deny_unknown_fields)]
30pub struct StoreDeviceExclusionRef {
31    pub proposal: StoreDeviceExclusionProposalRef,
32    pub outcome_hash: ObjectHash,
33    pub object: ExactObjectRef,
34}
35
36#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
37#[serde(deny_unknown_fields)]
38pub struct StoreDeviceExclusionCancellationRef {
39    pub proposal: StoreDeviceExclusionProposalRef,
40    pub outcome_hash: ObjectHash,
41    pub object: ExactObjectRef,
42}
43
44#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
45#[serde(rename_all = "snake_case", deny_unknown_fields)]
46pub enum StoreDeviceExclusionOutcomeRef {
47    Excluded(StoreDeviceExclusionRef),
48    Cancelled(StoreDeviceExclusionCancellationRef),
49}
50
51#[derive(Debug)]
52pub struct VerifiedDeviceExclusionProposal {
53    pub reference: StoreDeviceExclusionProposalRef,
54    pub object: crate::objects::VerifiedObject<StoreDeviceExclusionProposal>,
55    pub target: StoreDeviceRegistration,
56    pub owner: StoreDeviceRegistration,
57}
58
59#[derive(Debug)]
60pub struct VerifiedDeviceExclusionOutcome {
61    pub object: crate::objects::VerifiedObject<StoreDeviceExclusionOutcome>,
62    pub owner: StoreDeviceRegistration,
63}
64
65impl StoreDeviceExclusionOutcomeRef {
66    pub fn proposal(&self) -> &StoreDeviceExclusionProposalRef {
67        match self {
68            Self::Excluded(reference) => &reference.proposal,
69            Self::Cancelled(reference) => &reference.proposal,
70        }
71    }
72
73    pub fn object(&self) -> &ExactObjectRef {
74        match self {
75            Self::Excluded(reference) => &reference.object,
76            Self::Cancelled(reference) => &reference.object,
77        }
78    }
79
80    pub fn from_outcome(
81        outcome: &StoreDeviceExclusionOutcome,
82        proposal: &StoreDeviceExclusionProposal,
83        object: ExactObjectRef,
84    ) -> Result<Self, StoreProtocolError> {
85        if object.slot() != &proposal.outcome_slot
86            || outcome.proposal().proposal_id != proposal.proposal_id
87        {
88            return Err(StoreProtocolError::DeviceStateMismatch);
89        }
90        Ok(match outcome {
91            StoreDeviceExclusionOutcome::Excluded(exclusion) => {
92                Self::Excluded(StoreDeviceExclusionRef {
93                    proposal: exclusion.proposal.clone(),
94                    outcome_hash: exclusion.outcome_hash(),
95                    object,
96                })
97            }
98            StoreDeviceExclusionOutcome::Cancelled(cancellation) => {
99                Self::Cancelled(StoreDeviceExclusionCancellationRef {
100                    proposal: cancellation.proposal.clone(),
101                    outcome_hash: cancellation.outcome_hash(),
102                    object,
103                })
104            }
105        })
106    }
107
108    pub fn outcome_hash(&self) -> ObjectHash {
109        match self {
110            Self::Excluded(reference) => reference.outcome_hash,
111            Self::Cancelled(reference) => reference.outcome_hash,
112        }
113    }
114}
115
116/// The wire body of a device-exclusion proposal. Every field here is signed.
117#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
118#[serde(deny_unknown_fields)]
119pub struct StoreDeviceExclusionProposalBody {
120    pub store_root_hash: ObjectHash,
121    pub proposal_id: StoreDeviceExclusionProposalId,
122    pub target: StoreDeviceRegistrationRef,
123    pub outcome_slot: ObjectSlot,
124    pub owner_registration: StoreDeviceRegistrationRef,
125    pub owner_grant: MembershipGrantId,
126}
127
128impl SignedBody for StoreDeviceExclusionProposalBody {
129    const DOMAIN: &'static [u8] = DEVICE_EXCLUSION_PROPOSAL_DOMAIN;
130}
131
132pub type StoreDeviceExclusionProposal = Signed<StoreDeviceExclusionProposalBody>;
133
134#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
135#[serde(rename_all = "snake_case", deny_unknown_fields)]
136pub enum StoreDeviceExclusionOutcome {
137    Excluded(StoreDeviceExclusion),
138    Cancelled(StoreDeviceExclusionCancellation),
139}
140
141/// The wire body of an owner's withdrawal of an exclusion proposal. Every field
142/// here is signed.
143#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
144#[serde(deny_unknown_fields)]
145pub struct StoreDeviceExclusionCancellationBody {
146    pub store_root_hash: ObjectHash,
147    pub proposal: StoreDeviceExclusionProposalRef,
148    pub owner_registration: StoreDeviceRegistrationRef,
149    pub owner_grant: MembershipGrantId,
150}
151
152impl SignedBody for StoreDeviceExclusionCancellationBody {
153    const DOMAIN: &'static [u8] = DEVICE_EXCLUSION_CANCELLATION_DOMAIN;
154}
155
156pub type StoreDeviceExclusionCancellation = Signed<StoreDeviceExclusionCancellationBody>;
157
158/// The wire body of a device's exclusion. Every field here is signed.
159#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
160#[serde(deny_unknown_fields)]
161pub struct StoreDeviceExclusionBody {
162    pub store_root_hash: ObjectHash,
163    pub proposal: StoreDeviceExclusionProposalRef,
164    pub target: StoreDeviceRegistrationRef,
165    pub owner_registration: StoreDeviceRegistrationRef,
166    pub owner_grant: MembershipGrantId,
167}
168
169impl SignedBody for StoreDeviceExclusionBody {
170    const DOMAIN: &'static [u8] = DEVICE_EXCLUSION_DOMAIN;
171}
172
173pub type StoreDeviceExclusion = Signed<StoreDeviceExclusionBody>;
174
175impl StoreDeviceExclusionProposal {
176    #[allow(clippy::too_many_arguments)]
177    pub fn signed(
178        store_root_hash: ObjectHash,
179        proposal_id: StoreDeviceExclusionProposalId,
180        target: StoreDeviceRegistrationRef,
181        target_registration: &StoreDeviceRegistration,
182        outcome_slot: ObjectSlot,
183        owner_registration: StoreDeviceRegistrationRef,
184        owner_grant: MembershipGrantId,
185        owner: &StoreDeviceRegistration,
186        owner_device_signer: &UserKeypair,
187    ) -> Result<Self, StoreProtocolError> {
188        owner_registration.verify_registration(owner)?;
189        target.verify_registration(target_registration)?;
190        if keys::public_key_hex(owner_device_signer) != owner.device_signing_pubkey
191            || owner.store_root.store_root_hash != store_root_hash
192            || target_registration.store_root.store_root_hash != store_root_hash
193        {
194            return Err(StoreProtocolError::InvalidSignature);
195        }
196        let expected_outcome = format!(
197            "{}.json",
198            device_exclusion_outcome_semantic_prefix(target.device_id, proposal_id)
199        );
200        if outcome_slot.logical_key() != expected_outcome {
201            return Err(StoreProtocolError::RelocatedSlot {
202                expected: expected_outcome,
203                actual: outcome_slot.logical_key().to_string(),
204            });
205        }
206        Ok(Signed::sign(
207            StoreDeviceExclusionProposalBody {
208                store_root_hash,
209                proposal_id,
210                target,
211                outcome_slot,
212                owner_registration,
213                owner_grant,
214            },
215            owner_device_signer,
216        ))
217    }
218
219    pub fn proposal_hash(&self) -> ObjectHash {
220        self.hash()
221    }
222
223    pub fn parse_at(
224        bytes: &[u8],
225        expected: &StoreDeviceExclusionProposalRef,
226        target: &StoreDeviceRegistration,
227        owner: &StoreDeviceRegistration,
228    ) -> Result<Self, StoreProtocolError> {
229        let proposal: Self = crate::objects::decode_protocol_object(bytes)?;
230        expected.verify_proposal(&proposal)?;
231        proposal.target.verify_registration(target)?;
232        proposal.owner_registration.verify_registration(owner)?;
233        let expected_outcome = format!(
234            "{}.json",
235            device_exclusion_outcome_semantic_prefix(
236                proposal.target.device_id,
237                proposal.proposal_id,
238            )
239        );
240        if proposal.outcome_slot.logical_key() != expected_outcome {
241            return Err(StoreProtocolError::RelocatedSlot {
242                expected: expected_outcome,
243                actual: proposal.outcome_slot.logical_key().to_string(),
244            });
245        }
246        if proposal.store_root_hash != owner.store_root.store_root_hash
247            || proposal.store_root_hash != target.store_root.store_root_hash
248        {
249            return Err(StoreProtocolError::InvalidSignature);
250        }
251        proposal.verify_by(&owner.device_signing_pubkey)?;
252        Ok(proposal)
253    }
254}
255
256impl StoreDeviceExclusionProposalRef {
257    pub fn from_proposal(
258        proposal: &StoreDeviceExclusionProposal,
259        object: ExactObjectRef,
260    ) -> Result<Self, StoreProtocolError> {
261        let reference = Self {
262            proposal_id: proposal.proposal_id,
263            target: proposal.target.clone(),
264            proposal_hash: proposal.proposal_hash(),
265            object,
266        };
267        reference.validate_path()?;
268        Ok(reference)
269    }
270
271    pub fn validate_path(&self) -> Result<(), StoreProtocolError> {
272        let expected = format!(
273            "{}.json",
274            device_exclusion_proposal_semantic_prefix(
275                self.target.device_id,
276                self.proposal_id,
277                self.proposal_hash,
278            )
279        );
280        if self.object.slot().logical_key() != expected {
281            return Err(StoreProtocolError::RelocatedSlot {
282                expected,
283                actual: self.object.slot().logical_key().to_string(),
284            });
285        }
286        Ok(())
287    }
288
289    pub fn verify_proposal(
290        &self,
291        proposal: &StoreDeviceExclusionProposal,
292    ) -> Result<(), StoreProtocolError> {
293        self.validate_path()?;
294        if self.proposal_id != proposal.proposal_id
295            || self.target != proposal.target
296            || self.proposal_hash != proposal.proposal_hash()
297        {
298            return Err(StoreProtocolError::DeviceStateMismatch);
299        }
300        Ok(())
301    }
302}
303
304impl StoreDeviceExclusionCancellation {
305    pub fn signed(
306        proposal: StoreDeviceExclusionProposalRef,
307        proposal_value: &StoreDeviceExclusionProposal,
308        owner_registration: StoreDeviceRegistrationRef,
309        owner_grant: MembershipGrantId,
310        owner: &StoreDeviceRegistration,
311        owner_device_signer: &UserKeypair,
312    ) -> Result<Self, StoreProtocolError> {
313        owner_registration.verify_registration(owner)?;
314        if keys::public_key_hex(owner_device_signer) != owner.device_signing_pubkey
315            || proposal.proposal_hash != proposal_value.proposal_hash()
316            || proposal.target != proposal_value.target
317            || proposal_value.store_root_hash != owner.store_root.store_root_hash
318        {
319            return Err(StoreProtocolError::InvalidSignature);
320        }
321        Ok(Signed::sign(
322            StoreDeviceExclusionCancellationBody {
323                store_root_hash: owner.store_root.store_root_hash,
324                proposal,
325                owner_registration,
326                owner_grant,
327            },
328            owner_device_signer,
329        ))
330    }
331
332    pub fn outcome_hash(&self) -> ObjectHash {
333        self.hash()
334    }
335}
336
337impl StoreDeviceExclusion {
338    #[allow(clippy::too_many_arguments)]
339    pub fn signed(
340        proposal: StoreDeviceExclusionProposalRef,
341        proposal_value: &StoreDeviceExclusionProposal,
342        target: StoreDeviceRegistrationRef,
343        target_registration: &StoreDeviceRegistration,
344        owner_registration: StoreDeviceRegistrationRef,
345        owner_grant: MembershipGrantId,
346        owner: &StoreDeviceRegistration,
347        owner_device_signer: &UserKeypair,
348    ) -> Result<Self, StoreProtocolError> {
349        owner_registration.verify_registration(owner)?;
350        target.verify_registration(target_registration)?;
351        if keys::public_key_hex(owner_device_signer) != owner.device_signing_pubkey
352            || proposal.target != target
353            || proposal.proposal_hash != proposal_value.proposal_hash()
354            || proposal.target != proposal_value.target
355            || target_registration.store_root.store_root_hash != owner.store_root.store_root_hash
356        {
357            return Err(StoreProtocolError::InvalidSignature);
358        }
359        Ok(Signed::sign(
360            StoreDeviceExclusionBody {
361                store_root_hash: owner.store_root.store_root_hash,
362                proposal,
363                target,
364                owner_registration,
365                owner_grant,
366            },
367            owner_device_signer,
368        ))
369    }
370
371    pub fn outcome_hash(&self) -> ObjectHash {
372        self.hash()
373    }
374}
375
376impl StoreDeviceExclusionOutcome {
377    pub fn outcome_hash(&self) -> ObjectHash {
378        match self {
379            Self::Excluded(exclusion) => exclusion.outcome_hash(),
380            Self::Cancelled(cancellation) => cancellation.outcome_hash(),
381        }
382    }
383
384    pub fn proposal(&self) -> &StoreDeviceExclusionProposalRef {
385        match self {
386            Self::Excluded(exclusion) => &exclusion.proposal,
387            Self::Cancelled(cancellation) => &cancellation.proposal,
388        }
389    }
390
391    pub fn to_bytes(&self) -> Vec<u8> {
392        serde_json::to_vec(self).expect("Store device exclusion outcome serialization cannot fail")
393    }
394
395    pub fn parse_at(
396        bytes: &[u8],
397        expected: &StoreDeviceExclusionOutcomeRef,
398        proposal: &StoreDeviceExclusionProposal,
399        target: &StoreDeviceRegistration,
400        owner: &StoreDeviceRegistration,
401    ) -> Result<Self, StoreProtocolError> {
402        let outcome: Self = crate::objects::decode_protocol_object(bytes)?;
403        if outcome.proposal().proposal_id != proposal.proposal_id
404            || outcome.proposal().proposal_hash != proposal.proposal_hash()
405            || outcome.proposal().target != proposal.target
406            || expected.proposal() != outcome.proposal()
407            || expected.object().slot() != &proposal.outcome_slot
408            || expected.outcome_hash() != outcome.outcome_hash()
409        {
410            return Err(StoreProtocolError::DeviceStateMismatch);
411        }
412        match &outcome {
413            Self::Excluded(exclusion) => {
414                exclusion.target.verify_registration(target)?;
415                exclusion.owner_registration.verify_registration(owner)?;
416                if exclusion.store_root_hash != proposal.store_root_hash
417                    || exclusion.store_root_hash != target.store_root.store_root_hash
418                    || exclusion.target != proposal.target
419                {
420                    return Err(StoreProtocolError::InvalidSignature);
421                }
422                exclusion.verify_by(&owner.device_signing_pubkey)?;
423            }
424            Self::Cancelled(cancellation) => {
425                cancellation.owner_registration.verify_registration(owner)?;
426                if cancellation.store_root_hash != proposal.store_root_hash {
427                    return Err(StoreProtocolError::InvalidSignature);
428                }
429                cancellation.verify_by(&owner.device_signing_pubkey)?;
430            }
431        }
432        Ok(outcome)
433    }
434}