1use super::probe::*;
2use super::*;
3
4#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
5#[serde(rename_all = "snake_case", deny_unknown_fields)]
6pub enum CrossPrincipalProviderEvidence {
7 GoogleSharedDrive,
8 DropboxSharedNamespace,
9 OneDriveSharedFolder,
10 CloudKit(CloudKitAcceptedShare),
11}
12
13#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(deny_unknown_fields)]
15pub struct CloudKitAcceptedShare {
16 pub share: ExactObjectRef,
17 pub share_record_name: String,
18 pub owner_name: String,
19 pub zone_name: String,
20 pub participant_record_name: String,
21}
22
23#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
24#[serde(deny_unknown_fields)]
25pub struct CrossPrincipalProbeTranscript {
26 pub challenge: CrossPrincipalProbeChallenge,
27 pub response: CrossPrincipalProbeResponse,
28 pub administrator_read_peer_hash: ObjectHash,
29}
30
31#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
32#[serde(deny_unknown_fields)]
33pub struct CrossPrincipalProbeChallenge {
34 pub probe_id: ProviderProbeId,
35 pub administrator_object: ProbeExactObjectReceipt,
36 pub challenge_hash: ObjectHash,
37 pub administrator_signature: String,
38}
39
40#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
41#[serde(deny_unknown_fields)]
42pub struct CrossPrincipalProbeResponse {
43 pub challenge_hash: ObjectHash,
44 pub provider_evidence: CrossPrincipalProviderEvidence,
45 pub peer_object: ProbeExactObjectReceipt,
46 pub peer_read_administrator_hash: ObjectHash,
47 pub response_hash: ObjectHash,
48 pub peer_signature: String,
49}
50
51#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
52#[serde(deny_unknown_fields)]
53pub struct CrossPrincipalProbeReceipt {
54 pub transcript: CrossPrincipalProbeTranscript,
55 pub transcript_hash: ObjectHash,
56 pub administrator_completion_signature: String,
57}
58
59#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
60#[serde(deny_unknown_fields)]
61pub struct CrossPrincipalChallengeContext {
62 pub root: StoreRootRef,
63 pub attempt_id: DeviceJoinAttemptId,
64 pub access_request_hash: ObjectHash,
65 pub provider_admin_grant: ProviderAdminGrantId,
66 pub owner_registration: StoreDeviceRegistrationRef,
67 pub member_pubkey: String,
68 pub administrator_binding: ProviderDeviceBinding,
69 pub peer_binding: ProviderDeviceBinding,
70}
71
72#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
73#[serde(deny_unknown_fields)]
74pub struct CrossPrincipalResponseContext {
75 pub challenge: CrossPrincipalChallengeContext,
76 pub expected_registration_hash: ObjectHash,
77 pub response_slot: ObjectSlot,
78}
79
80#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
81#[serde(deny_unknown_fields)]
82pub struct DeviceJoinChallengePublicationAuthorization {
83 pub attempt_id: DeviceJoinAttemptId,
84 pub attempt_activation: StoreBatchCommitRef,
85}
86
87#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
88#[serde(deny_unknown_fields)]
89pub struct DeviceJoinChallengePublicationRecord {
90 pub challenge: CrossPrincipalProbeChallenge,
91 pub progress: DeviceJoinChallengePublicationProgress,
92}
93
94#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
95#[serde(rename_all = "snake_case", deny_unknown_fields)]
96pub enum DeviceJoinChallengePublicationProgress {
97 Prepared,
98 Published {
99 authorization: DeviceJoinChallengePublicationAuthorization,
100 },
101}
102
103#[async_trait]
104pub trait DeviceJoinChallengePublicationJournal: Send + Sync {
105 async fn prepare(
106 &self,
107 challenge: &CrossPrincipalProbeChallenge,
108 ) -> Result<DeviceJoinChallengePublicationRecord, StorageError>;
109
110 async fn claim_published(
114 &self,
115 authorization: &DeviceJoinChallengePublicationAuthorization,
116 challenge: &CrossPrincipalProbeChallenge,
117 ) -> Result<(), StorageError>;
118}
119
120impl CrossPrincipalProbeReceipt {
121 pub fn signed(
122 transcript: CrossPrincipalProbeTranscript,
123 context: &CrossPrincipalResponseContext,
124 store: &StoreProviderBinding,
125 administrator_signer: &dyn coven_keys::keys::DeviceSigningAuthority,
126 ) -> Result<Self, ProviderProbeError> {
127 validate_cross_transcript_payloads(&transcript, context)?;
128 let transcript_hash = cross_transcript_hash(store, context, &transcript);
129 Ok(Self {
130 transcript,
131 transcript_hash,
132 administrator_completion_signature: hex::encode(
133 administrator_signer.sign(transcript_hash.as_bytes()),
134 ),
135 })
136 }
137
138 pub fn verify(
139 &self,
140 context: &CrossPrincipalResponseContext,
141 store: &StoreProviderBinding,
142 administrator_signing_pubkey: &str,
143 peer_signing_pubkey: &str,
144 ) -> Result<(), ProviderProbeError> {
145 validate_cross_provider_evidence(
146 store,
147 &context.challenge.administrator_binding,
148 &context.challenge.peer_binding,
149 &self.transcript.response.provider_evidence,
150 )?;
151 self.transcript.challenge.verify(
152 &context.challenge,
153 store,
154 administrator_signing_pubkey,
155 )?;
156 self.transcript.response.verify(
157 &self.transcript.challenge,
158 context,
159 store,
160 administrator_signing_pubkey,
161 peer_signing_pubkey,
162 )?;
163 validate_cross_transcript_payloads(&self.transcript, context)?;
164 let expected_hash = cross_transcript_hash(store, context, &self.transcript);
165 if self.transcript_hash != expected_hash {
166 return invalid("cross-principal transcript hash does not match its join context");
167 }
168 if !coven_keys::keys::verify_signature_hex(
169 administrator_signing_pubkey,
170 &self.administrator_completion_signature,
171 self.transcript_hash.as_bytes(),
172 ) {
173 return invalid("cross-principal completion signature is invalid");
174 }
175 Ok(())
176 }
177}
178
179impl CrossPrincipalProbeChallenge {
180 pub fn verify(
181 &self,
182 context: &CrossPrincipalChallengeContext,
183 store: &StoreProviderBinding,
184 administrator_signing_pubkey: &str,
185 ) -> Result<(), ProviderProbeError> {
186 validate_cross_challenge_payload(self)?;
187 validate_cross_provider_evidence_context(store, context)?;
188 let expected_hash = cross_challenge_hash(store, context, self);
189 if self.challenge_hash != expected_hash {
190 return invalid("cross-principal challenge hash does not match its join context");
191 }
192 if !coven_keys::keys::verify_signature_hex(
193 administrator_signing_pubkey,
194 &self.administrator_signature,
195 self.challenge_hash.as_bytes(),
196 ) {
197 return invalid("cross-principal challenge signature is invalid");
198 }
199 Ok(())
200 }
201}
202
203impl CrossPrincipalProbeResponse {
204 pub fn verify(
205 &self,
206 challenge: &CrossPrincipalProbeChallenge,
207 context: &CrossPrincipalResponseContext,
208 store: &StoreProviderBinding,
209 administrator_signing_pubkey: &str,
210 peer_signing_pubkey: &str,
211 ) -> Result<(), ProviderProbeError> {
212 challenge.verify(&context.challenge, store, administrator_signing_pubkey)?;
213 if context.challenge.member_pubkey != peer_signing_pubkey {
214 return invalid("cross-principal response signer is not the joining member");
215 }
216 validate_cross_provider_evidence(
217 store,
218 &context.challenge.administrator_binding,
219 &context.challenge.peer_binding,
220 &self.provider_evidence,
221 )?;
222 validate_cross_response_payload(self, challenge, context)?;
223 let expected_hash = cross_response_hash(store, context, challenge, self);
224 if self.response_hash != expected_hash {
225 return invalid("cross-principal response hash does not match its join context");
226 }
227 if !coven_keys::keys::verify_signature_hex(
228 peer_signing_pubkey,
229 &self.peer_signature,
230 self.response_hash.as_bytes(),
231 ) {
232 return invalid("cross-principal response signature is invalid");
233 }
234 Ok(())
235 }
236}
237
238pub(crate) fn cross_transcript_hash(
239 store: &StoreProviderBinding,
240 context: &CrossPrincipalResponseContext,
241 transcript: &CrossPrincipalProbeTranscript,
242) -> ObjectHash {
243 ObjectHash::digest(&domain_json(
244 CROSS_TRANSCRIPT_DOMAIN,
245 &(store, context, transcript),
246 ))
247}
248
249pub(crate) fn validate_cross_transcript_payloads(
250 transcript: &CrossPrincipalProbeTranscript,
251 context: &CrossPrincipalResponseContext,
252) -> Result<(), ProviderProbeError> {
253 validate_cross_challenge_payload(&transcript.challenge)?;
254 validate_cross_response_payload(&transcript.response, &transcript.challenge, context)?;
255 let peer = probe_payload(&transcript.challenge.probe_id, ProbePayloadLabel::CrossPeer);
256 if transcript.administrator_read_peer_hash != ObjectHash::digest(&peer) {
257 return invalid("cross-principal object, read, or deletion evidence is invalid");
258 }
259 Ok(())
260}
261
262pub fn cross_challenge_hash(
263 store: &StoreProviderBinding,
264 context: &CrossPrincipalChallengeContext,
265 challenge: &CrossPrincipalProbeChallenge,
266) -> ObjectHash {
267 ObjectHash::digest(&domain_json(
268 CROSS_CHALLENGE_DOMAIN,
269 &(
270 store,
271 context,
272 challenge.probe_id,
273 &challenge.administrator_object,
274 ),
275 ))
276}
277
278pub fn cross_response_hash(
279 store: &StoreProviderBinding,
280 context: &CrossPrincipalResponseContext,
281 challenge: &CrossPrincipalProbeChallenge,
282 response: &CrossPrincipalProbeResponse,
283) -> ObjectHash {
284 ObjectHash::digest(&domain_json(
285 CROSS_RESPONSE_DOMAIN,
286 &(
287 store,
288 context,
289 challenge.challenge_hash,
290 &response.provider_evidence,
291 &response.peer_object,
292 response.peer_read_administrator_hash,
293 ),
294 ))
295}
296
297pub(crate) fn validate_cross_challenge_payload(
298 challenge: &CrossPrincipalProbeChallenge,
299) -> Result<(), ProviderProbeError> {
300 let expected_key = cross_administrator_logical_key(challenge.probe_id);
301 let payload = probe_payload(&challenge.probe_id, ProbePayloadLabel::CrossAdministrator);
302 validate_probe_exact_object(
303 &challenge.administrator_object,
304 &expected_key,
305 &payload,
306 "cross-principal challenge",
307 )
308}
309
310pub(crate) fn validate_cross_response_payload(
311 response: &CrossPrincipalProbeResponse,
312 challenge: &CrossPrincipalProbeChallenge,
313 context: &CrossPrincipalResponseContext,
314) -> Result<(), ProviderProbeError> {
315 let administrator = probe_payload(&challenge.probe_id, ProbePayloadLabel::CrossAdministrator);
316 let peer = probe_payload(&challenge.probe_id, ProbePayloadLabel::CrossPeer);
317 if response.challenge_hash != challenge.challenge_hash
318 || response.peer_object.slot != context.response_slot
319 || response.peer_read_administrator_hash != ObjectHash::digest(&administrator)
320 {
321 return invalid(
322 "cross-principal response disagrees with its challenge or response context",
323 );
324 }
325 validate_probe_exact_object(
326 &response.peer_object,
327 &cross_peer_logical_key(challenge.probe_id),
328 &peer,
329 "cross-principal response",
330 )
331}
332
333pub(super) fn cross_administrator_logical_key(probe_id: ProviderProbeId) -> String {
334 format!(
335 "__coven_probe__/cross/{}/administrator",
336 hex::encode(probe_id.as_bytes())
337 )
338}
339
340pub fn cross_peer_logical_key(probe_id: ProviderProbeId) -> String {
341 format!(
342 "__coven_probe__/cross/{}/peer",
343 hex::encode(probe_id.as_bytes())
344 )
345}
346
347pub fn validate_cross_provider_evidence_context(
348 store: &StoreProviderBinding,
349 context: &CrossPrincipalChallengeContext,
350) -> Result<(), ProviderProbeError> {
351 context
352 .administrator_binding
353 .validate_for(store)
354 .map_err(ProviderProbeError::Storage)?;
355 context
356 .peer_binding
357 .validate_for(store)
358 .map_err(ProviderProbeError::Storage)?;
359 if context.administrator_binding == context.peer_binding {
360 return invalid("cross-principal context uses the same provider principal twice");
361 }
362 Ok(())
363}
364
365pub fn validate_cross_provider_evidence(
366 store: &StoreProviderBinding,
367 administrator: &ProviderDeviceBinding,
368 peer: &ProviderDeviceBinding,
369 evidence: &CrossPrincipalProviderEvidence,
370) -> Result<(), ProviderProbeError> {
371 administrator
372 .validate_for(store)
373 .map_err(ProviderProbeError::Storage)?;
374 peer.validate_for(store)
375 .map_err(ProviderProbeError::Storage)?;
376 if administrator == peer {
377 return invalid("cross-principal receipt uses the same provider principal twice");
378 }
379 let compatible = matches!(
380 (store, evidence),
381 (
382 StoreProviderBinding::GoogleDrive {
383 corpus: crate::objects::GoogleDriveCorpus::SharedDrive { .. }
384 },
385 CrossPrincipalProviderEvidence::GoogleSharedDrive
386 ) | (
387 StoreProviderBinding::Dropbox { .. },
388 CrossPrincipalProviderEvidence::DropboxSharedNamespace
389 ) | (
390 StoreProviderBinding::OneDrive { .. },
391 CrossPrincipalProviderEvidence::OneDriveSharedFolder
392 ) | (
393 StoreProviderBinding::CloudKit { .. },
394 CrossPrincipalProviderEvidence::CloudKit(_)
395 )
396 );
397 if !compatible {
398 return invalid("provider binding does not permit the cross-principal evidence");
399 }
400 if let (
401 StoreProviderBinding::CloudKit {
402 owner_name,
403 zone_name,
404 ..
405 },
406 CrossPrincipalProviderEvidence::CloudKit(accepted),
407 ) = (store, evidence)
408 {
409 let crate::objects::ProviderPrincipalId::CloudKitSharedZoneParticipant { record_name } =
410 &peer.principal
411 else {
412 return invalid("CloudKit peer is not a shared-zone participant");
413 };
414 if accepted.owner_name != *owner_name
415 || accepted.zone_name != *zone_name
416 || accepted.participant_record_name != *record_name
417 || accepted.share_record_name.is_empty()
418 {
419 return invalid("CloudKit accepted-share evidence differs from the Store binding");
420 }
421 }
422 Ok(())
423}