1use serde::{Deserialize, Serialize};
4
5use crate::objects::{ExactObjectRef, PreparedExactObject, StorageError};
6use crate::store_commit::ObjectHash;
7use coven_keys::keys::{self, UserKeypair};
8
9#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
21#[serde(deny_unknown_fields)]
22pub struct WrappedStoreKey {
23 pub author_pubkey: String,
25 pub generation: u64,
28 pub sealed: String,
30 pub signature: String,
32}
33
34#[derive(Serialize)]
39struct WrappedKeyFields<'a> {
40 store_id: &'a str,
41 recipient_pubkey: &'a str,
42 generation: u64,
43 author_pubkey: &'a str,
44 sealed: &'a str,
45}
46
47#[derive(Debug, thiserror::Error)]
53pub enum WrappedKeyError {
54 #[error("signature does not verify against an authorized store owner")]
61 SignatureMismatch,
62 #[error("sealed box is not valid hex")]
65 MalformedSealed,
66}
67
68#[derive(Debug, thiserror::Error)]
69pub enum WrappedKeyringError {
70 #[error("{0}")]
71 Authentication(#[from] WrappedKeyError),
72 #[error("decrypt sealed Store keyring: {0}")]
73 Decryption(#[from] coven_keys::keys::KeyError),
74 #[error("decode Store keyring payload: {0}")]
75 Payload(#[from] coven_keys::encryption::EncryptionError),
76 #[error(
77 "wrapped Store-key ref declares generation {reference}, but its keyring declares {payload}"
78 )]
79 GenerationMismatch { reference: u64, payload: u64 },
80}
81
82impl WrappedStoreKey {
83 pub fn seal_keyring(
84 store_id: &str,
85 recipient_pubkey: &str,
86 recipient_x25519_pk: &[u8; keys::CURVE25519_PUBLICKEYBYTES],
87 encryption: &coven_keys::encryption::EncryptionService,
88 owner: &UserKeypair,
89 ) -> Result<Self, coven_keys::encryption::EncryptionError> {
90 let payload = encryption.to_keyring_payload()?;
91 Ok(Self::signed(
92 store_id,
93 recipient_pubkey,
94 encryption.current_generation(),
95 keys::seal_box_encrypt(&payload, recipient_x25519_pk),
96 owner,
97 ))
98 }
99
100 pub fn signed(
104 store_id: &str,
105 recipient_pubkey: &str,
106 generation: u64,
107 sealed: Vec<u8>,
108 owner: &UserKeypair,
109 ) -> Self {
110 let author_pubkey = hex::encode(owner.public_key());
111 let sealed_hex = hex::encode(sealed);
112 let payload = wrapped_key_signing_payload(
113 store_id,
114 recipient_pubkey,
115 generation,
116 &author_pubkey,
117 &sealed_hex,
118 );
119 let (_, signature) = keys::sign_hex(owner, &payload);
120 WrappedStoreKey {
121 author_pubkey,
122 generation,
123 sealed: sealed_hex,
124 signature,
125 }
126 }
127
128 pub fn verify_and_unwrap<'a>(
137 &self,
138 store_id: &str,
139 recipient_pubkey: &str,
140 expected_owners: impl IntoIterator<Item = &'a str>,
141 ) -> Result<Vec<u8>, WrappedKeyError> {
142 let payload = wrapped_key_signing_payload(
143 store_id,
144 recipient_pubkey,
145 self.generation,
146 &self.author_pubkey,
147 &self.sealed,
148 );
149 if !expected_owners
152 .into_iter()
153 .any(|owner| owner == self.author_pubkey)
154 || !keys::verify_signature_hex(&self.author_pubkey, &self.signature, &payload)
155 {
156 return Err(WrappedKeyError::SignatureMismatch);
157 }
158 hex::decode(&self.sealed).map_err(|_| WrappedKeyError::MalformedSealed)
161 }
162
163 pub fn verify_and_open_keyring<'a>(
164 &self,
165 store_id: &str,
166 recipient_pubkey: &str,
167 expected_owners: impl IntoIterator<Item = &'a str>,
168 expected_generation: u64,
169 recipient: &dyn coven_keys::keys::IdentityKeyAuthority,
170 ) -> Result<coven_keys::encryption::EncryptionService, WrappedKeyringError> {
171 let sealed = self.verify_and_unwrap(store_id, recipient_pubkey, expected_owners)?;
172 let plaintext = keys::seal_box_decrypt(&sealed, &recipient.to_x25519_secret_key())?;
173 let keyring = coven_keys::encryption::EncryptionService::from_keyring_payload(plaintext)?;
174 if keyring.current_generation() != expected_generation {
175 return Err(WrappedKeyringError::GenerationMismatch {
176 reference: expected_generation,
177 payload: keyring.current_generation(),
178 });
179 }
180 Ok(keyring)
181 }
182}
183
184fn wrapped_key_signing_payload(
185 store_id: &str,
186 recipient_pubkey: &str,
187 generation: u64,
188 author_pubkey: &str,
189 sealed_hex: &str,
190) -> Vec<u8> {
191 let fields = WrappedKeyFields {
192 store_id,
193 recipient_pubkey,
194 generation,
195 author_pubkey,
196 sealed: sealed_hex,
197 };
198 serde_json::to_vec(&fields).expect("wrapped key fields serialization cannot fail")
199}
200
201#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
202#[serde(deny_unknown_fields)]
203pub struct WrappedStoreKeyRef {
204 pub owner_pubkey: String,
205 pub recipient_pubkey: String,
206 pub generation: u64,
207 pub wrap_hash: ObjectHash,
208 pub object: ExactObjectRef,
209}
210
211impl WrappedStoreKeyRef {
212 pub fn semantic_prefix(&self) -> String {
213 format!(
214 "keys/{}/{}/{}/{}",
215 self.owner_pubkey, self.recipient_pubkey, self.generation, self.wrap_hash
216 )
217 }
218
219 pub fn validate_identity(&self) -> Result<(), StorageError> {
220 for pubkey in [&self.owner_pubkey, &self.recipient_pubkey] {
221 coven_foundation::store_dir::validate_path_token(pubkey)?;
222 let bytes = hex::decode(pubkey).map_err(|_| {
223 StorageError::InvalidContent(
224 "wrapped Store-key ref contains an invalid public key".to_string(),
225 )
226 })?;
227 if bytes.len() != keys::SIGN_PUBLICKEYBYTES || hex::encode(&bytes) != *pubkey {
228 return Err(StorageError::InvalidContent(
229 "wrapped Store-key ref contains an invalid public key".to_string(),
230 ));
231 }
232 }
233 if self.generation == 0
234 || self.object.slot().logical_key() != format!("{}.json", self.semantic_prefix())
235 {
236 return Err(StorageError::InvalidContent(
237 "wrapped Store-key ref has an invalid semantic identity".to_string(),
238 ));
239 }
240 Ok(())
241 }
242
243 pub fn validate_value(
244 &self,
245 value: &WrappedStoreKey,
246 bytes: &[u8],
247 ) -> Result<(), StorageError> {
248 self.validate_identity()?;
249 if value.author_pubkey != self.owner_pubkey
250 || value.generation != self.generation
251 || ObjectHash::digest(bytes) != self.wrap_hash
252 {
253 return Err(StorageError::InvalidContent(
254 "wrapped Store-key ref does not match its exact value".to_string(),
255 ));
256 }
257 Ok(())
258 }
259}
260
261#[derive(Clone, Debug, Serialize, Deserialize)]
262#[serde(deny_unknown_fields)]
263pub struct PreparedWrappedStoreKey {
264 pub reference: WrappedStoreKeyRef,
265 pub object: PreparedExactObject,
266}
267
268impl PreparedWrappedStoreKey {
269 pub fn validate(&self) -> Result<WrappedStoreKey, StorageError> {
270 if self.reference.object != *self.object.reference() {
271 return Err(StorageError::InvalidContent(
272 "prepared wrapped Store key carries a different exact reference".to_string(),
273 ));
274 }
275 let value: WrappedStoreKey = serde_json::from_slice(self.object.stored_bytes())?;
276 self.reference
277 .validate_value(&value, self.object.stored_bytes())?;
278 Ok(value)
279 }
280}
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285
286 #[test]
287 fn wrapped_key_round_trips_and_returns_sealed_bytes() {
288 let owner = UserKeypair::generate();
289 let owner_hex = hex::encode(owner.public_key());
290 let sealed = vec![1u8, 2, 3, 4, 5];
291 let wrapped = WrappedStoreKey::signed("lib", "recipient-pk", 1, sealed.clone(), &owner);
292
293 let json = serde_json::to_vec(&wrapped).expect("serialize wrapped key");
295 let parsed: WrappedStoreKey = serde_json::from_slice(&json).expect("parse wrapped key");
296 assert_eq!(
297 parsed
298 .verify_and_unwrap("lib", "recipient-pk", std::iter::once(owner_hex.as_str()))
299 .unwrap(),
300 sealed,
301 );
302 }
303
304 #[test]
305 fn wrapped_key_signed_by_non_owner_is_refused() {
306 let signer = UserKeypair::generate();
311 let pinned_owner = UserKeypair::generate();
312 let pinned_owner_hex = hex::encode(pinned_owner.public_key());
313 let sealed = vec![9u8; 32];
314 let wrapped = WrappedStoreKey::signed("lib", "recipient-pk", 1, sealed, &signer);
315
316 assert!(
317 matches!(
318 wrapped.verify_and_unwrap(
319 "lib",
320 "recipient-pk",
321 std::iter::once(pinned_owner_hex.as_str())
322 ),
323 Err(WrappedKeyError::SignatureMismatch),
324 ),
325 "a key not signed by the pinned owner must be refused",
326 );
327 }
328
329 #[test]
330 fn wrapped_key_rejects_rebinding() {
331 let owner = UserKeypair::generate();
332 let owner_hex = hex::encode(owner.public_key());
333 let sealed = vec![9u8; 32];
334 let wrapped = WrappedStoreKey::signed("lib", "recipient-pk", 1, sealed, &owner);
335
336 assert!(
340 matches!(
341 wrapped.verify_and_unwrap(
342 "other-lib",
343 "recipient-pk",
344 std::iter::once(owner_hex.as_str())
345 ),
346 Err(WrappedKeyError::SignatureMismatch),
347 ),
348 "must reject a key replayed into a different store",
349 );
350 assert!(
351 matches!(
352 wrapped.verify_and_unwrap(
353 "lib",
354 "other-recipient",
355 std::iter::once(owner_hex.as_str())
356 ),
357 Err(WrappedKeyError::SignatureMismatch),
358 ),
359 "must reject a key relocated to another recipient's slot",
360 );
361 }
362
363 #[test]
364 fn wrapped_key_rejects_author_tamper() {
365 let owner = UserKeypair::generate();
366 let other_owner = UserKeypair::generate();
367 let owner_hex = hex::encode(owner.public_key());
368 let other_owner_hex = hex::encode(other_owner.public_key());
369 let sealed = vec![9u8; 32];
370 let mut wrapped = WrappedStoreKey::signed("lib", "recipient-pk", 1, sealed, &owner);
371 assert!(
372 wrapped
373 .verify_and_unwrap("lib", "recipient-pk", std::iter::once(owner_hex.as_str()))
374 .is_ok(),
375 "freshly signed author verifies",
376 );
377
378 wrapped.author_pubkey = other_owner_hex.clone();
379 assert!(
380 matches!(
381 wrapped.verify_and_unwrap(
382 "lib",
383 "recipient-pk",
384 [owner_hex.as_str(), other_owner_hex.as_str()]
385 ),
386 Err(WrappedKeyError::SignatureMismatch),
387 ),
388 "tampering with the named author invalidates the signature",
389 );
390 }
391
392 #[test]
393 fn wrapped_key_rejects_generation_tamper() {
394 let owner = UserKeypair::generate();
395 let owner_hex = hex::encode(owner.public_key());
396 let mut wrapped = WrappedStoreKey::signed("lib", "recipient-pk", 3, vec![9u8; 32], &owner);
397
398 wrapped.generation = 99;
402 assert!(
403 matches!(
404 wrapped.verify_and_unwrap(
405 "lib",
406 "recipient-pk",
407 std::iter::once(owner_hex.as_str())
408 ),
409 Err(WrappedKeyError::SignatureMismatch),
410 ),
411 "tampering with the claimed generation invalidates the signature",
412 );
413 }
414
415 #[test]
416 fn wrapped_key_malformed_signature_fails_closed() {
417 let owner = UserKeypair::generate();
418 let owner_hex = hex::encode(owner.public_key());
419 let mut wrapped = WrappedStoreKey::signed("lib", "recipient-pk", 1, vec![1u8; 4], &owner);
420
421 wrapped.signature = "not-hex!!".to_string();
423 assert!(matches!(
424 wrapped.verify_and_unwrap("lib", "recipient-pk", std::iter::once(owner_hex.as_str())),
425 Err(WrappedKeyError::SignatureMismatch),
426 ));
427 }
428
429 #[test]
430 fn wrapped_key_malformed_sealed_is_distinguished() {
431 let owner = UserKeypair::generate();
436 let owner_hex = hex::encode(owner.public_key());
437
438 let mut wrapped = WrappedStoreKey {
439 author_pubkey: owner_hex.clone(),
440 generation: 1,
441 sealed: "not-hex!!".to_string(),
442 signature: String::new(),
443 };
444 let payload = wrapped_key_signing_payload(
445 "lib",
446 "recipient-pk",
447 wrapped.generation,
448 &wrapped.author_pubkey,
449 &wrapped.sealed,
450 );
451 let (_, signature) = keys::sign_hex(&owner, &payload);
452 wrapped.signature = signature;
453
454 assert!(matches!(
455 wrapped.verify_and_unwrap("lib", "recipient-pk", std::iter::once(owner_hex.as_str())),
456 Err(WrappedKeyError::MalformedSealed),
457 ));
458 }
459
460 #[test]
461 fn wrapped_ref_generation_must_match_its_decrypted_keyring() {
462 let owner = UserKeypair::generate();
463 let recipient = UserKeypair::generate();
464 let recipient_pubkey = keys::public_key_hex(&recipient);
465 let keyring = coven_keys::encryption::EncryptionService::from_key([7; 32]);
466 let sealed = keys::seal_box_encrypt(
467 &keyring.to_keyring_payload().expect("serialize keyring"),
468 &recipient.to_x25519_public_key(),
469 );
470 let wrapped = WrappedStoreKey::signed(
471 "wrapped-generation-store",
472 &recipient_pubkey,
473 2,
474 sealed,
475 &owner,
476 );
477
478 assert!(matches!(
479 wrapped.verify_and_open_keyring(
480 "wrapped-generation-store",
481 &recipient_pubkey,
482 std::iter::once(keys::public_key_hex(&owner).as_str()),
483 2,
484 &recipient,
485 ),
486 Err(WrappedKeyringError::GenerationMismatch {
487 reference: 2,
488 payload: 1,
489 }),
490 ));
491 }
492}