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 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#[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#[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}