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#[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 frozen_device_state: StoreDeviceStateRef,
124 pub outcome_slot: ObjectSlot,
125 pub owner_registration: StoreDeviceRegistrationRef,
126 pub owner_grant: MembershipGrantId,
127}
128
129impl SignedBody for StoreDeviceExclusionProposalBody {
130 const DOMAIN: &'static [u8] = DEVICE_EXCLUSION_PROPOSAL_DOMAIN;
131}
132
133pub type StoreDeviceExclusionProposal = Signed<StoreDeviceExclusionProposalBody>;
134
135#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
136#[serde(rename_all = "snake_case", deny_unknown_fields)]
137pub enum StoreDeviceExclusionOutcome {
138 Excluded(StoreDeviceExclusion),
139 Cancelled(StoreDeviceExclusionCancellation),
140}
141
142#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
145#[serde(deny_unknown_fields)]
146pub struct StoreDeviceExclusionCancellationBody {
147 pub store_root_hash: ObjectHash,
148 pub proposal: StoreDeviceExclusionProposalRef,
149 pub owner_registration: StoreDeviceRegistrationRef,
150 pub owner_grant: MembershipGrantId,
151}
152
153impl SignedBody for StoreDeviceExclusionCancellationBody {
154 const DOMAIN: &'static [u8] = DEVICE_EXCLUSION_CANCELLATION_DOMAIN;
155}
156
157pub type StoreDeviceExclusionCancellation = Signed<StoreDeviceExclusionCancellationBody>;
158
159#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
161#[serde(deny_unknown_fields)]
162pub struct StoreDeviceExclusionBody {
163 pub store_root_hash: ObjectHash,
164 pub proposal: StoreDeviceExclusionProposalRef,
165 pub target: StoreDeviceRegistrationRef,
166 pub proof: StoreDeviceExclusionProof,
167 pub owner_registration: StoreDeviceRegistrationRef,
168 pub owner_grant: MembershipGrantId,
169}
170
171impl SignedBody for StoreDeviceExclusionBody {
172 const DOMAIN: &'static [u8] = DEVICE_EXCLUSION_DOMAIN;
173}
174
175pub type StoreDeviceExclusion = Signed<StoreDeviceExclusionBody>;
176
177#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
178#[serde(deny_unknown_fields)]
179pub struct StoreDeviceExclusionProof {
180 pub frozen_device_state: StoreDeviceStateRef,
181 pub remaining_device_acks: Vec<StoreAckRef>,
182 pub cutoff: StoreHistoryCut,
183}
184
185impl StoreDeviceExclusionProposal {
186 #[allow(clippy::too_many_arguments)]
187 pub fn signed(
188 store_root_hash: ObjectHash,
189 proposal_id: StoreDeviceExclusionProposalId,
190 target: StoreDeviceRegistrationRef,
191 target_registration: &StoreDeviceRegistration,
192 frozen_device_state: StoreDeviceStateRef,
193 outcome_slot: ObjectSlot,
194 owner_registration: StoreDeviceRegistrationRef,
195 owner_grant: MembershipGrantId,
196 owner: &StoreDeviceRegistration,
197 owner_device_signer: &UserKeypair,
198 ) -> Result<Self, StoreProtocolError> {
199 owner_registration.verify_registration(owner)?;
200 target.verify_registration(target_registration)?;
201 if keys::public_key_hex(owner_device_signer) != owner.device_signing_pubkey
202 || owner.store_root.store_root_hash != store_root_hash
203 || target_registration.store_root.store_root_hash != store_root_hash
204 {
205 return Err(StoreProtocolError::InvalidSignature);
206 }
207 let expected_outcome = format!(
208 "{}.json",
209 device_exclusion_outcome_semantic_prefix(target.device_id, proposal_id)
210 );
211 if outcome_slot.logical_key() != expected_outcome {
212 return Err(StoreProtocolError::RelocatedSlot {
213 expected: expected_outcome,
214 actual: outcome_slot.logical_key().to_string(),
215 });
216 }
217 validate_store_device_state_ref(&frozen_device_state)?;
218 Ok(Signed::sign(
219 StoreDeviceExclusionProposalBody {
220 store_root_hash,
221 proposal_id,
222 target,
223 frozen_device_state,
224 outcome_slot,
225 owner_registration,
226 owner_grant,
227 },
228 owner_device_signer,
229 ))
230 }
231
232 pub fn proposal_hash(&self) -> ObjectHash {
233 self.hash()
234 }
235
236 pub fn parse_at(
237 bytes: &[u8],
238 expected: &StoreDeviceExclusionProposalRef,
239 target: &StoreDeviceRegistration,
240 owner: &StoreDeviceRegistration,
241 ) -> Result<Self, StoreProtocolError> {
242 let proposal: Self = crate::objects::decode_protocol_object(bytes)?;
243 expected.verify_proposal(&proposal)?;
244 proposal.target.verify_registration(target)?;
245 proposal.owner_registration.verify_registration(owner)?;
246 validate_store_device_state_ref(&proposal.frozen_device_state)?;
247 let expected_outcome = format!(
248 "{}.json",
249 device_exclusion_outcome_semantic_prefix(
250 proposal.target.device_id,
251 proposal.proposal_id,
252 )
253 );
254 if proposal.outcome_slot.logical_key() != expected_outcome {
255 return Err(StoreProtocolError::RelocatedSlot {
256 expected: expected_outcome,
257 actual: proposal.outcome_slot.logical_key().to_string(),
258 });
259 }
260 if proposal.store_root_hash != owner.store_root.store_root_hash
261 || proposal.store_root_hash != target.store_root.store_root_hash
262 {
263 return Err(StoreProtocolError::InvalidSignature);
264 }
265 proposal.verify_by(&owner.device_signing_pubkey)?;
266 Ok(proposal)
267 }
268}
269
270impl StoreDeviceExclusionProposalRef {
271 pub fn from_proposal(
272 proposal: &StoreDeviceExclusionProposal,
273 object: ExactObjectRef,
274 ) -> Result<Self, StoreProtocolError> {
275 let reference = Self {
276 proposal_id: proposal.proposal_id,
277 target: proposal.target.clone(),
278 proposal_hash: proposal.proposal_hash(),
279 object,
280 };
281 reference.validate_path()?;
282 Ok(reference)
283 }
284
285 pub fn validate_path(&self) -> Result<(), StoreProtocolError> {
286 let expected = format!(
287 "{}.json",
288 device_exclusion_proposal_semantic_prefix(
289 self.target.device_id,
290 self.proposal_id,
291 self.proposal_hash,
292 )
293 );
294 if self.object.slot().logical_key() != expected {
295 return Err(StoreProtocolError::RelocatedSlot {
296 expected,
297 actual: self.object.slot().logical_key().to_string(),
298 });
299 }
300 Ok(())
301 }
302
303 pub fn verify_proposal(
304 &self,
305 proposal: &StoreDeviceExclusionProposal,
306 ) -> Result<(), StoreProtocolError> {
307 self.validate_path()?;
308 if self.proposal_id != proposal.proposal_id
309 || self.target != proposal.target
310 || self.proposal_hash != proposal.proposal_hash()
311 {
312 return Err(StoreProtocolError::DeviceStateMismatch);
313 }
314 Ok(())
315 }
316}
317
318impl StoreDeviceExclusionCancellation {
319 pub fn signed(
320 proposal: StoreDeviceExclusionProposalRef,
321 proposal_value: &StoreDeviceExclusionProposal,
322 owner_registration: StoreDeviceRegistrationRef,
323 owner_grant: MembershipGrantId,
324 owner: &StoreDeviceRegistration,
325 owner_device_signer: &UserKeypair,
326 ) -> Result<Self, StoreProtocolError> {
327 owner_registration.verify_registration(owner)?;
328 if keys::public_key_hex(owner_device_signer) != owner.device_signing_pubkey
329 || proposal.proposal_hash != proposal_value.proposal_hash()
330 || proposal.target != proposal_value.target
331 || proposal_value.store_root_hash != owner.store_root.store_root_hash
332 {
333 return Err(StoreProtocolError::InvalidSignature);
334 }
335 Ok(Signed::sign(
336 StoreDeviceExclusionCancellationBody {
337 store_root_hash: owner.store_root.store_root_hash,
338 proposal,
339 owner_registration,
340 owner_grant,
341 },
342 owner_device_signer,
343 ))
344 }
345
346 pub fn outcome_hash(&self) -> ObjectHash {
347 self.hash()
348 }
349}
350
351impl StoreDeviceExclusion {
352 #[allow(clippy::too_many_arguments)]
353 pub fn signed(
354 proposal: StoreDeviceExclusionProposalRef,
355 proposal_value: &StoreDeviceExclusionProposal,
356 target: StoreDeviceRegistrationRef,
357 target_registration: &StoreDeviceRegistration,
358 proof: StoreDeviceExclusionProof,
359 owner_registration: StoreDeviceRegistrationRef,
360 owner_grant: MembershipGrantId,
361 owner: &StoreDeviceRegistration,
362 owner_device_signer: &UserKeypair,
363 ) -> Result<Self, StoreProtocolError> {
364 owner_registration.verify_registration(owner)?;
365 target.verify_registration(target_registration)?;
366 if keys::public_key_hex(owner_device_signer) != owner.device_signing_pubkey
367 || proposal.target != target
368 || proposal.proposal_hash != proposal_value.proposal_hash()
369 || proposal.target != proposal_value.target
370 || target_registration.store_root.store_root_hash != owner.store_root.store_root_hash
371 {
372 return Err(StoreProtocolError::InvalidSignature);
373 }
374 validate_device_exclusion_proof(&proof)?;
375 Ok(Signed::sign(
376 StoreDeviceExclusionBody {
377 store_root_hash: owner.store_root.store_root_hash,
378 proposal,
379 target,
380 proof,
381 owner_registration,
382 owner_grant,
383 },
384 owner_device_signer,
385 ))
386 }
387
388 pub fn outcome_hash(&self) -> ObjectHash {
389 self.hash()
390 }
391}
392
393impl StoreDeviceExclusionOutcome {
394 pub fn outcome_hash(&self) -> ObjectHash {
395 match self {
396 Self::Excluded(exclusion) => exclusion.outcome_hash(),
397 Self::Cancelled(cancellation) => cancellation.outcome_hash(),
398 }
399 }
400
401 pub fn proposal(&self) -> &StoreDeviceExclusionProposalRef {
402 match self {
403 Self::Excluded(exclusion) => &exclusion.proposal,
404 Self::Cancelled(cancellation) => &cancellation.proposal,
405 }
406 }
407
408 pub fn to_bytes(&self) -> Vec<u8> {
409 serde_json::to_vec(self).expect("Store device exclusion outcome serialization cannot fail")
410 }
411
412 pub fn parse_at(
413 bytes: &[u8],
414 expected: &StoreDeviceExclusionOutcomeRef,
415 proposal: &StoreDeviceExclusionProposal,
416 target: &StoreDeviceRegistration,
417 owner: &StoreDeviceRegistration,
418 ) -> Result<Self, StoreProtocolError> {
419 let outcome: Self = crate::objects::decode_protocol_object(bytes)?;
420 if outcome.proposal().proposal_id != proposal.proposal_id
421 || outcome.proposal().proposal_hash != proposal.proposal_hash()
422 || outcome.proposal().target != proposal.target
423 || expected.proposal() != outcome.proposal()
424 || expected.object().slot() != &proposal.outcome_slot
425 || expected.outcome_hash() != outcome.outcome_hash()
426 {
427 return Err(StoreProtocolError::DeviceStateMismatch);
428 }
429 match &outcome {
430 Self::Excluded(exclusion) => {
431 exclusion.target.verify_registration(target)?;
432 exclusion.owner_registration.verify_registration(owner)?;
433 validate_device_exclusion_proof(&exclusion.proof)?;
434 if exclusion.store_root_hash != proposal.store_root_hash
435 || exclusion.store_root_hash != target.store_root.store_root_hash
436 || exclusion.target != proposal.target
437 {
438 return Err(StoreProtocolError::InvalidSignature);
439 }
440 exclusion.verify_by(&owner.device_signing_pubkey)?;
441 }
442 Self::Cancelled(cancellation) => {
443 cancellation.owner_registration.verify_registration(owner)?;
444 if cancellation.store_root_hash != proposal.store_root_hash {
445 return Err(StoreProtocolError::InvalidSignature);
446 }
447 cancellation.verify_by(&owner.device_signing_pubkey)?;
448 }
449 }
450 Ok(outcome)
451 }
452}
453
454fn validate_device_exclusion_proof(
455 proof: &StoreDeviceExclusionProof,
456) -> Result<(), StoreProtocolError> {
457 if proof
458 .remaining_device_acks
459 .windows(2)
460 .any(|pair| pair[0] >= pair[1])
461 {
462 return Err(StoreProtocolError::DeviceStateMismatch);
463 }
464 validate_store_device_state_ref(&proof.frozen_device_state)?;
465 validate_store_history_cut(&proof.cutoff)
466}