coven_protocol/store_commit/
device_join.rs1use super::*;
2
3const STORE_DEVICE_ID_DOMAIN: &[u8] = b"coven.store-device-id.v1\0";
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
10#[serde(transparent)]
11pub struct StoreDeviceId(ObjectHash);
12
13impl StoreDeviceId {
14 pub fn derive(store_root: &StoreRootRef, origin: &StoreDeviceRegistrationOrigin) -> Self {
15 let mut material = STORE_DEVICE_ID_DOMAIN.to_vec();
16 material.extend(
17 serde_json::to_vec(&(store_root, origin.external_id()))
18 .expect("Store device identity serialization cannot fail"),
19 );
20 Self(ObjectHash::digest(&material))
21 }
22}
23
24impl fmt::Display for StoreDeviceId {
25 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
26 fmt::Display::fmt(&self.0, formatter)
27 }
28}
29
30impl FromStr for StoreDeviceId {
31 type Err = StoreProtocolError;
32
33 fn from_str(value: &str) -> Result<Self, Self::Err> {
34 Ok(Self(value.parse()?))
35 }
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
39#[serde(transparent)]
40pub struct StoreCreationId(ObjectHash);
41
42impl StoreCreationId {
43 pub fn from_random_bytes(bytes: [u8; 32]) -> Self {
44 Self(ObjectHash::from_digest(bytes))
45 }
46
47 pub(super) fn object_hash(self) -> ObjectHash {
48 self.0
49 }
50
51 #[cfg(any(test, feature = "test-utils"))]
52 #[cfg(any(test, feature = "test-utils"))]
53 pub fn from_nonce(nonce: &str) -> Self {
54 Self(ObjectHash::digest(nonce.as_bytes()))
55 }
56}
57
58impl fmt::Display for StoreCreationId {
59 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
60 fmt::Display::fmt(&self.0, formatter)
61 }
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
65#[serde(transparent)]
66pub struct DeviceJoinAttemptId(ObjectHash);
67
68impl DeviceJoinAttemptId {
69 pub fn from_hash(hash: ObjectHash) -> Self {
70 Self(hash)
71 }
72
73 pub(super) fn object_hash(self) -> ObjectHash {
74 self.0
75 }
76}
77
78impl fmt::Display for DeviceJoinAttemptId {
79 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
80 fmt::Display::fmt(&self.0, formatter)
81 }
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
85#[serde(transparent)]
86pub struct DeviceRecoveryId(ObjectHash);
87
88impl DeviceRecoveryId {
89 pub fn from_hash(hash: ObjectHash) -> Self {
90 Self(hash)
91 }
92
93 pub(super) fn object_hash(self) -> ObjectHash {
94 self.0
95 }
96}
97
98impl fmt::Display for DeviceRecoveryId {
99 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
100 fmt::Display::fmt(&self.0, formatter)
101 }
102}
103
104#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
107#[serde(deny_unknown_fields)]
108pub struct DeviceReadinessProofBody {
109 pub store_root_hash: ObjectHash,
110 pub attempt_id: DeviceJoinAttemptId,
111 pub registration: StoreDeviceRegistrationRef,
112 pub initial_ack: StoreAckRef,
113 pub bootstrap_cut: StoreHistoryCut,
114}
115
116impl SignedBody for DeviceReadinessProofBody {
117 const DOMAIN: &'static [u8] = DEVICE_READINESS_DOMAIN;
118}
119
120pub type DeviceReadinessProof = Signed<DeviceReadinessProofBody>;
121
122impl DeviceReadinessProof {
123 pub fn signed(
124 attempt_id: DeviceJoinAttemptId,
125 registration: StoreDeviceRegistrationRef,
126 initial_ack: StoreAckRef,
127 bootstrap_cut: StoreHistoryCut,
128 registration_value: &StoreDeviceRegistration,
129 device_signer: &UserKeypair,
130 ) -> Result<Self, StoreProtocolError> {
131 registration.verify_registration(registration_value)?;
132 if keys::public_key_hex(device_signer) != registration_value.device_signing_pubkey {
133 return Err(StoreProtocolError::InvalidSignature);
134 }
135 let body = DeviceReadinessProofBody {
136 store_root_hash: registration_value.store_root.store_root_hash,
137 attempt_id,
138 registration,
139 initial_ack,
140 bootstrap_cut,
141 };
142 validate_store_history_cut(&body.bootstrap_cut)?;
143 Ok(Signed::sign(body, device_signer))
144 }
145
146 pub fn verify(
152 &self,
153 attempt_id: DeviceJoinAttemptId,
154 attempt_cut: &StoreHistoryCut,
155 registration: &StoreDeviceRegistration,
156 initial_ack_ref: &StoreAckRef,
157 initial_ack: &StoreAck,
158 ) -> Result<(), StoreProtocolError> {
159 if self.attempt_id != attempt_id
160 || self.store_root_hash != registration.store_root.store_root_hash
161 || self.registration.device_id != registration.device_id
162 || &self.bootstrap_cut != attempt_cut
163 {
164 return Err(StoreProtocolError::DeviceReadinessMismatch);
165 }
166 self.registration.verify_registration(registration)?;
167 if initial_ack.registration != self.registration
168 || initial_ack.sequence != 1
169 || initial_ack.successor.predecessor.is_some()
170 || initial_ack_ref != &self.initial_ack
171 || initial_ack_ref.registration != self.registration
172 || initial_ack_ref.sequence != initial_ack.sequence
173 || initial_ack_ref.ack_hash != initial_ack.ack_hash()
174 || initial_ack.store_cut != self.bootstrap_cut
175 {
176 return Err(StoreProtocolError::DeviceReadinessMismatch);
177 }
178 validate_store_history_cut(&self.bootstrap_cut)?;
179 self.verify_by(®istration.device_signing_pubkey)
180 }
181}
182
183#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
184#[serde(deny_unknown_fields)]
185pub struct OwnerRecoveryNodeRef {
186 pub owner_pubkey: String,
187 pub owner_grant: MembershipGrantId,
188 pub sequence: u64,
189 pub node_hash: ObjectHash,
190 pub object: ExactObjectRef,
191}
192
193#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
194#[serde(transparent)]
195pub struct OwnerRecoveryActivationId(ObjectHash);
196
197impl OwnerRecoveryActivationId {
198 pub fn derive(
199 root: &StoreRootRef,
200 owner_pubkey: &str,
201 owner_grant: &MembershipGrantId,
202 anchor: &GrantStreamAnchor,
203 ) -> Result<Self, StoreProtocolError> {
204 if !matches!(anchor, GrantStreamAnchor::OwnerRecovery { .. }) {
205 return Err(StoreProtocolError::OwnerRecoveryMismatch);
206 }
207 Ok(Self(ObjectHash::digest(&domain_json(
208 b"coven.owner-recovery-activation.v1\0",
209 &(root, owner_pubkey, owner_grant, anchor),
210 ))))
211 }
212}
213
214#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
215#[serde(rename_all = "snake_case", deny_unknown_fields)]
216pub enum OwnerRecoveryPosition {
217 BeforeFirst {
218 activation: OwnerRecoveryActivationId,
219 },
220 At {
221 node: OwnerRecoveryNodeRef,
222 },
223}
224
225impl OwnerRecoveryPosition {
226 pub fn merge(&self, other: &Self) -> Result<Self, super::StoreProtocolError> {
233 match (self, other) {
234 (Self::BeforeFirst { activation }, Self::BeforeFirst { activation: other })
235 if activation == other =>
236 {
237 Ok(self.clone())
238 }
239 (Self::BeforeFirst { .. }, Self::BeforeFirst { .. }) => {
240 Err(super::StoreProtocolError::OwnerRecoveryMismatch)
241 }
242 (Self::BeforeFirst { .. }, Self::At { .. }) => Ok(other.clone()),
243 (Self::At { .. }, Self::BeforeFirst { .. }) => Ok(self.clone()),
244 (Self::At { node }, Self::At { node: other_node }) => {
245 match node.sequence.cmp(&other_node.sequence) {
246 std::cmp::Ordering::Less => Ok(other.clone()),
247 std::cmp::Ordering::Greater => Ok(self.clone()),
248 std::cmp::Ordering::Equal if node == other_node => Ok(self.clone()),
249 std::cmp::Ordering::Equal => {
250 Err(super::StoreProtocolError::OwnerRecoveryMismatch)
251 }
252 }
253 }
254 }
255 }
256}
257
258#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
259#[serde(deny_unknown_fields)]
260pub struct OwnerRecoveryCursor {
261 pub owner_grant: MembershipGrantId,
262 pub position: OwnerRecoveryPosition,
263}
264
265#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
266#[serde(deny_unknown_fields)]
267pub struct DeviceJoinAbandonmentRef {
268 pub attempt_id: DeviceJoinAttemptId,
269 pub abandonment_hash: ObjectHash,
270 pub object: ExactObjectRef,
271}