1use nostr_sdk::prelude::nip44::v2::ConversationKey;
27use nostr_sdk::prelude::*;
28use serde::{Deserialize, Serialize};
29use sha2::{Digest, Sha256};
30use zeroize::Zeroizing;
31
32use super::cipher;
33use super::derive::{base_rekey_pseudonym, recipient_pseudonym, rekey_pseudonym, RekeyScope};
34use super::{ChannelId, CommunityId, Epoch, Pseudonym, ServerRootKey};
35use crate::stored_event::event_kind;
36
37#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
41pub struct RekeyBlob {
42 pub locator: String,
43 pub wrapped: String,
44}
45
46pub fn rekey_pairwise_secret(sk: &SecretKey, pk: &PublicKey) -> Result<[u8; 32], String> {
50 let ck = ConversationKey::derive(sk, pk).map_err(|e| format!("pairwise ECDH: {e}"))?;
51 let bytes = ck.as_bytes();
52 bytes
53 .try_into()
54 .map_err(|_| format!("conversation key is {} bytes, expected 32", bytes.len()))
55}
56
57fn bound_plaintext(scope: RekeyScope, epoch: Epoch, new_key: &[u8; 32]) -> Vec<u8> {
60 let mut pt = Vec::with_capacity(72);
61 pt.extend_from_slice(&scope.id32());
62 pt.extend_from_slice(&epoch.0.to_be_bytes());
63 pt.extend_from_slice(new_key);
64 pt
65}
66
67pub fn build_rekey_blob(
70 sender_sk: &SecretKey,
71 recipient_pk: &PublicKey,
72 scope: RekeyScope,
73 epoch: Epoch,
74 new_key: &[u8; 32],
75) -> Result<RekeyBlob, String> {
76 let secret = Zeroizing::new(rekey_pairwise_secret(sender_sk, recipient_pk)?);
79 let locator = recipient_pseudonym(&secret, scope, epoch).to_hex();
80 let pt = Zeroizing::new(bound_plaintext(scope, epoch, new_key));
81 let wrapped = cipher::seal(&secret, &pt).map_err(|e| format!("wrap rekey blob: {e}"))?;
82 Ok(RekeyBlob { locator, wrapped })
83}
84
85pub fn open_rekey_blob(
90 my_sk: &SecretKey,
91 sender_pk: &PublicKey,
92 scope: RekeyScope,
93 epoch: Epoch,
94 blob: &RekeyBlob,
95) -> Result<[u8; 32], String> {
96 let secret = Zeroizing::new(rekey_pairwise_secret(my_sk, sender_pk)?);
97 let expected = recipient_pseudonym(&secret, scope, epoch).to_hex();
100 if blob.locator != expected {
101 return Err("rekey blob locator does not match this recipient/scope/epoch".to_string());
102 }
103 let pt = Zeroizing::new(cipher::open(&secret, &blob.wrapped).map_err(|e| format!("open rekey blob: {e}"))?);
104 if pt.len() != 72 {
105 return Err(format!("rekey blob plaintext is {} bytes, expected 72", pt.len()));
106 }
107 if pt[..32] != scope.id32() {
110 return Err("rekey blob scope binding mismatch (splice)".to_string());
111 }
112 let mut epoch_be = [0u8; 8];
113 epoch_be.copy_from_slice(&pt[32..40]);
114 if u64::from_be_bytes(epoch_be) != epoch.0 {
115 return Err("rekey blob epoch binding mismatch (splice)".to_string());
116 }
117 let mut new_key = [0u8; 32];
118 new_key.copy_from_slice(&pt[40..72]);
119 Ok(new_key)
120}
121
122const PROTOCOL_VERSION: &str = "1";
126const TAG_VERSION: &str = "v";
127const TAG_SCOPE: &str = "scope";
128const TAG_NEW_EPOCH: &str = "newepoch";
129const TAG_PREV_EPOCH: &str = "prevepoch";
130const TAG_PREV_COMMIT: &str = "prevcommit";
131
132#[cfg(test)]
135const STRFRY_MAX_EVENT_SIZE: usize = 65536;
136
137pub(crate) const MAX_REKEY_BLOBS: usize = 120;
146
147pub fn epoch_key_commitment(prev_epoch: Epoch, prev_key: &[u8; 32]) -> [u8; 32] {
153 let mut h = Sha256::new();
154 h.update(b"vector-community/v1/epoch-key-commitment");
155 h.update(prev_epoch.0.to_be_bytes());
156 h.update(prev_key);
157 h.finalize().into()
158}
159
160#[derive(Debug, Clone)]
165pub struct ParsedRekey {
166 pub rotator: PublicKey,
168 pub scope: RekeyScope,
170 pub new_epoch: Epoch,
172 pub prev_epoch: Epoch,
174 pub prev_key_commitment: [u8; 32],
177 pub blobs: Vec<RekeyBlob>,
179}
180
181fn scope_to_hex(scope: RekeyScope) -> String {
185 crate::simd::hex::bytes_to_hex_32(&scope.id32())
186}
187
188fn scope_from_hex(hex: &str) -> Option<RekeyScope> {
189 if hex.len() != 64 || !hex.bytes().all(|b| b.is_ascii_hexdigit()) {
190 return None;
191 }
192 let bytes = crate::simd::hex::hex_to_bytes_32(hex);
193 Some(if bytes == [0u8; 32] {
194 RekeyScope::ServerRoot
195 } else {
196 RekeyScope::Channel(ChannelId(bytes))
197 })
198}
199
200fn build_rekey_inner(
205 rotator: &Keys,
206 scope: RekeyScope,
207 new_epoch: Epoch,
208 prev_epoch: Epoch,
209 prev_key_commitment: &[u8; 32],
210 blobs: &[RekeyBlob],
211) -> Result<Event, String> {
212 if new_epoch.0 <= prev_epoch.0 {
213 return Err(format!("rekey new_epoch {} must exceed prev_epoch {}", new_epoch.0, prev_epoch.0));
214 }
215 let blobs_json = serde_json::to_string(blobs).map_err(|e| format!("serialize blobs: {e}"))?;
216 EventBuilder::new(Kind::Custom(event_kind::COMMUNITY_REKEY), blobs_json)
217 .tags([
218 Tag::custom(TAG_SCOPE, [scope_to_hex(scope)]),
219 Tag::custom(TAG_NEW_EPOCH, [new_epoch.0.to_string()]),
220 Tag::custom(TAG_PREV_EPOCH, [prev_epoch.0.to_string()]),
221 Tag::custom(TAG_PREV_COMMIT, [crate::simd::hex::bytes_to_hex_32(prev_key_commitment)]),
222 ])
223 .finalize(rotator)
224 .map_err(|e| format!("sign rekey inner: {e}"))
225}
226
227fn seal_rekey_outer(ephemeral: &Keys, inner: &Event, envelope_key: &[u8; 32], address: &Pseudonym) -> Result<Event, String> {
231 let content = cipher::seal(envelope_key, inner.as_json().as_bytes()).map_err(|e| format!("seal rekey: {e}"))?;
232 EventBuilder::new(Kind::Custom(event_kind::COMMUNITY_REKEY), content)
233 .tags([
234 Tag::custom("z", [address.to_hex()]),
235 Tag::custom(TAG_VERSION, [PROTOCOL_VERSION.to_string()]),
236 ])
237 .finalize(ephemeral)
238 .map_err(|e| format!("sign rekey outer: {e}"))
239}
240
241#[allow(clippy::too_many_arguments)]
252pub fn build_channel_rekey_event(
253 ephemeral: &Keys,
254 rotator: &Keys,
255 server_root: &[u8; 32],
256 channel_id: &ChannelId,
257 new_epoch: Epoch,
258 prev_epoch: Epoch,
259 prev_key_commitment: &[u8; 32],
260 blobs: &[RekeyBlob],
261) -> Result<Event, String> {
262 let inner = build_rekey_inner(rotator, RekeyScope::Channel(*channel_id), new_epoch, prev_epoch, prev_key_commitment, blobs)?;
263 let address = rekey_pseudonym(&ServerRootKey(*server_root), channel_id, new_epoch);
266 seal_rekey_outer(ephemeral, &inner, server_root, &address)
267}
268
269#[allow(clippy::too_many_arguments)]
281pub fn build_server_root_rekey_event(
282 ephemeral: &Keys,
283 rotator: &Keys,
284 prior_root: &[u8; 32],
285 community_id: &CommunityId,
286 new_epoch: Epoch,
287 prev_epoch: Epoch,
288 prev_key_commitment: &[u8; 32],
289 blobs: &[RekeyBlob],
290) -> Result<Event, String> {
291 let inner = build_rekey_inner(rotator, RekeyScope::ServerRoot, new_epoch, prev_epoch, prev_key_commitment, blobs)?;
292 let address = base_rekey_pseudonym(&ServerRootKey(*prior_root), community_id, new_epoch);
293 seal_rekey_outer(ephemeral, &inner, prior_root, &address)
294}
295
296pub fn open_rekey_event(outer: &Event, server_root: &[u8; 32]) -> Result<ParsedRekey, String> {
301 if outer.kind.as_u16() != event_kind::COMMUNITY_REKEY {
302 return Err("not a rekey outer (kind != 3303)".to_string());
303 }
304 match find_unique_tag(outer, TAG_VERSION)?.as_deref() {
305 Some(PROTOCOL_VERSION) => {}
306 other => return Err(format!("unsupported rekey version: {other:?}")),
307 }
308 let plaintext = cipher::open(server_root, &outer.content).map_err(|e| format!("open rekey: {e}"))?;
309 let json = String::from_utf8(plaintext).map_err(|e| format!("rekey inner utf8: {e}"))?;
310 let inner = Event::from_json(&json).map_err(|e| format!("rekey inner parse: {e}"))?;
311
312 inner.verify().map_err(|_| "rekey inner signature invalid".to_string())?;
314 if inner.kind.as_u16() != event_kind::COMMUNITY_REKEY {
315 return Err("rekey inner is not kind 3303".to_string());
316 }
317
318 let scope = find_unique_tag(&inner, TAG_SCOPE)?
319 .and_then(|h| scope_from_hex(&h))
320 .ok_or("rekey missing/invalid scope")?;
321 let new_epoch = Epoch(parse_u64_tag(&inner, TAG_NEW_EPOCH)?);
322 let prev_epoch = Epoch(parse_u64_tag(&inner, TAG_PREV_EPOCH)?);
323 let prev_hex = find_unique_tag(&inner, TAG_PREV_COMMIT)?.ok_or("rekey missing prev-commit")?;
324 if prev_hex.len() != 64 || !prev_hex.bytes().all(|b| b.is_ascii_hexdigit()) {
325 return Err("rekey prev-commit is not 32-byte hex".to_string());
326 }
327 let prev_key_commitment = crate::simd::hex::hex_to_bytes_32(&prev_hex);
328
329 let blobs: Vec<RekeyBlob> =
330 serde_json::from_str(&inner.content).map_err(|e| format!("parse rekey blobs: {e}"))?;
331 if blobs.len() > MAX_REKEY_BLOBS {
332 return Err(format!("rekey carries {} blobs, over the cap", blobs.len()));
333 }
334
335 Ok(ParsedRekey {
336 rotator: inner.pubkey,
337 scope,
338 new_epoch,
339 prev_epoch,
340 prev_key_commitment,
341 blobs,
342 })
343}
344
345fn find_unique_tag(event: &Event, name: &str) -> Result<Option<String>, String> {
348 let mut found = None;
349 for t in event.tags.iter() {
350 let s = t.as_slice();
351 if s.len() >= 2 && s[0] == name {
352 if found.is_some() {
353 return Err(format!("duplicate rekey tag: {name}"));
354 }
355 found = Some(s[1].clone());
356 }
357 }
358 Ok(found)
359}
360
361fn parse_u64_tag(event: &Event, name: &str) -> Result<u64, String> {
362 find_unique_tag(event, name)?
363 .ok_or_else(|| format!("rekey missing tag: {name}"))?
364 .parse::<u64>()
365 .map_err(|_| format!("rekey tag {name} is not a u64"))
366}
367
368#[cfg(test)]
369mod tests {
370 use super::*;
371
372 #[test]
378 fn max_rekey_blobs_event_stays_under_relay_size_limit() {
379 let rotator = Keys::generate();
380 let blobs: Vec<RekeyBlob> = (0..MAX_REKEY_BLOBS)
381 .map(|_| {
382 let r = Keys::generate();
383 build_rekey_blob(rotator.secret_key(), &r.public_key(), RekeyScope::ServerRoot, Epoch(1), &[0xCDu8; 32]).unwrap()
384 })
385 .collect();
386 let outer = build_server_root_rekey_event(
387 &Keys::generate(), &rotator, &[0x07u8; 32], &CommunityId([0x09u8; 32]), Epoch(1), Epoch(0), &[0u8; 32], &blobs,
388 )
389 .unwrap();
390 let size = outer.as_json().len();
391 assert!(
392 size <= STRFRY_MAX_EVENT_SIZE,
393 "a full {MAX_REKEY_BLOBS}-blob rekey event is {size} bytes; must stay <= {STRFRY_MAX_EVENT_SIZE} (strfry maxEventSize)"
394 );
395 }
396
397 fn sk(byte: u8) -> SecretKey {
398 SecretKey::from_slice(&[byte; 32]).unwrap()
399 }
400
401 #[test]
402 fn bound_plaintext_layout_is_frozen() {
403 let pt = bound_plaintext(RekeyScope::ServerRoot, Epoch(1), &[0xABu8; 32]);
406 let expected = format!("{}{}{}", "00".repeat(32), "0000000000000001", "ab".repeat(32));
407 assert_eq!(crate::simd::hex::bytes_to_hex_string(&pt), expected);
408 let pt2 = bound_plaintext(RekeyScope::Channel(ChannelId([0x11u8; 32])), Epoch(0x0102), &[0xCDu8; 32]);
410 let expected2 = format!("{}{}{}", "11".repeat(32), "0000000000000102", "cd".repeat(32));
411 assert_eq!(crate::simd::hex::bytes_to_hex_string(&pt2), expected2);
412 }
413
414 #[test]
415 fn pairwise_secret_regression_pin() {
416 let a = Keys::new(sk(1));
421 let b = Keys::new(sk(2));
422 let secret = rekey_pairwise_secret(a.secret_key(), &b.public_key()).unwrap();
423 assert_eq!(crate::simd::hex::bytes_to_hex_string(&secret), GOLDEN_PAIRWISE_SECRET);
424 }
425
426 const GOLDEN_PAIRWISE_SECRET: &str =
429 "59c6d24d9c3a7bf8ca4cec54031a3e2ecfaa553452a2b2fa3147e31ee55f33d5";
430
431 #[test]
432 fn pairwise_secret_is_symmetric_and_deterministic() {
433 let a = Keys::new(sk(1));
434 let b = Keys::new(sk(2));
435 let from_a = rekey_pairwise_secret(a.secret_key(), &b.public_key()).unwrap();
436 let from_b = rekey_pairwise_secret(b.secret_key(), &a.public_key()).unwrap();
437 assert_eq!(from_a, from_b, "ECDH is symmetric: both sides derive the same secret");
438 assert_eq!(from_a, rekey_pairwise_secret(a.secret_key(), &b.public_key()).unwrap());
440 let c = Keys::new(sk(3));
442 assert_ne!(from_a, rekey_pairwise_secret(a.secret_key(), &c.public_key()).unwrap());
443 }
444
445 #[test]
446 fn blob_round_trips_server_root_scope() {
447 let sender = Keys::new(sk(7));
448 let recipient = Keys::new(sk(8));
449 let new_key = [0xABu8; 32];
450 let blob = build_rekey_blob(
451 sender.secret_key(), &recipient.public_key(), RekeyScope::ServerRoot, Epoch(1), &new_key,
452 )
453 .unwrap();
454 let got = open_rekey_blob(
455 recipient.secret_key(), &sender.public_key(), RekeyScope::ServerRoot, Epoch(1), &blob,
456 )
457 .unwrap();
458 assert_eq!(got, new_key, "the recipient recovers the fresh key");
459 }
460
461 #[test]
462 fn blob_round_trips_channel_scope() {
463 let sender = Keys::new(sk(7));
464 let recipient = Keys::new(sk(8));
465 let chan = RekeyScope::Channel(ChannelId([0x42u8; 32]));
466 let new_key = [0xCDu8; 32];
467 let blob = build_rekey_blob(sender.secret_key(), &recipient.public_key(), chan, Epoch(5), &new_key).unwrap();
468 let got = open_rekey_blob(recipient.secret_key(), &sender.public_key(), chan, Epoch(5), &blob).unwrap();
469 assert_eq!(got, new_key);
470 }
471
472 #[test]
473 fn locator_is_the_recipient_pseudonym() {
474 let sender = Keys::new(sk(7));
477 let recipient = Keys::new(sk(8));
478 let secret = rekey_pairwise_secret(sender.secret_key(), &recipient.public_key()).unwrap();
479 let blob = build_rekey_blob(
480 sender.secret_key(), &recipient.public_key(), RekeyScope::ServerRoot, Epoch(2), &[1u8; 32],
481 )
482 .unwrap();
483 assert_eq!(blob.locator, recipient_pseudonym(&secret, RekeyScope::ServerRoot, Epoch(2)).to_hex());
484 }
485
486 #[test]
487 fn wrong_sender_cannot_open() {
488 let sender = Keys::new(sk(7));
491 let recipient = Keys::new(sk(8));
492 let impostor = Keys::new(sk(9));
493 let blob = build_rekey_blob(
494 sender.secret_key(), &recipient.public_key(), RekeyScope::ServerRoot, Epoch(1), &[2u8; 32],
495 )
496 .unwrap();
497 let err = open_rekey_blob(
498 recipient.secret_key(), &impostor.public_key(), RekeyScope::ServerRoot, Epoch(1), &blob,
499 );
500 assert!(err.is_err(), "pairing against the wrong sender must fail");
501 }
502
503 #[test]
504 fn non_recipient_cannot_open() {
505 let sender = Keys::new(sk(7));
508 let recipient = Keys::new(sk(8));
509 let other = Keys::new(sk(10));
510 let blob = build_rekey_blob(
511 sender.secret_key(), &recipient.public_key(), RekeyScope::ServerRoot, Epoch(1), &[3u8; 32],
512 )
513 .unwrap();
514 assert!(open_rekey_blob(
515 other.secret_key(), &sender.public_key(), RekeyScope::ServerRoot, Epoch(1), &blob,
516 )
517 .is_err());
518 }
519
520 #[test]
521 fn scope_splice_is_rejected() {
522 let sender = Keys::new(sk(7));
525 let recipient = Keys::new(sk(8));
526 let blob = build_rekey_blob(
527 sender.secret_key(), &recipient.public_key(), RekeyScope::ServerRoot, Epoch(1), &[4u8; 32],
528 )
529 .unwrap();
530 let chan = RekeyScope::Channel(ChannelId([0x42u8; 32]));
531 assert!(open_rekey_blob(recipient.secret_key(), &sender.public_key(), chan, Epoch(1), &blob).is_err());
532 }
533
534 #[test]
535 fn epoch_splice_is_rejected() {
536 let sender = Keys::new(sk(7));
537 let recipient = Keys::new(sk(8));
538 let blob = build_rekey_blob(
539 sender.secret_key(), &recipient.public_key(), RekeyScope::ServerRoot, Epoch(1), &[5u8; 32],
540 )
541 .unwrap();
542 assert!(open_rekey_blob(
543 recipient.secret_key(), &sender.public_key(), RekeyScope::ServerRoot, Epoch(2), &blob,
544 )
545 .is_err());
546 }
547
548 #[test]
549 fn relocated_blob_is_rejected() {
550 let sender = Keys::new(sk(7));
553 let recipient = Keys::new(sk(8));
554 let mut blob = build_rekey_blob(
555 sender.secret_key(), &recipient.public_key(), RekeyScope::ServerRoot, Epoch(1), &[6u8; 32],
556 )
557 .unwrap();
558 blob.locator = "ff".repeat(32);
559 assert!(open_rekey_blob(
560 recipient.secret_key(), &sender.public_key(), RekeyScope::ServerRoot, Epoch(1), &blob,
561 )
562 .is_err());
563 }
564
565 #[test]
566 fn tampered_ciphertext_is_rejected() {
567 let sender = Keys::new(sk(7));
568 let recipient = Keys::new(sk(8));
569 let mut blob = build_rekey_blob(
570 sender.secret_key(), &recipient.public_key(), RekeyScope::ServerRoot, Epoch(1), &[7u8; 32],
571 )
572 .unwrap();
573 let mut bytes = base64_simd::STANDARD.decode_to_vec(blob.wrapped.as_bytes()).unwrap();
575 let mid = bytes.len() / 2;
576 bytes[mid] ^= 0xff;
577 blob.wrapped = base64_simd::STANDARD.encode_to_string(&bytes);
578 assert!(open_rekey_blob(
579 recipient.secret_key(), &sender.public_key(), RekeyScope::ServerRoot, Epoch(1), &blob,
580 )
581 .is_err());
582 }
583
584 const SR: [u8; 32] = [0x55u8; 32];
588 const CHAN: [u8; 32] = [0x42u8; 32];
589
590 #[test]
591 fn epoch_key_commitment_golden_and_binds_epoch() {
592 let c = epoch_key_commitment(Epoch(1), &[0x33u8; 32]);
594 assert_eq!(crate::simd::hex::bytes_to_hex_32(&c), GOLDEN_EPOCH_COMMITMENT);
595 assert_ne!(c, epoch_key_commitment(Epoch(2), &[0x33u8; 32]));
597 assert_ne!(c, epoch_key_commitment(Epoch(1), &[0x34u8; 32]));
599 }
600
601 #[test]
602 fn channel_rekey_round_trips() {
603 let rotator = Keys::new(sk(1));
604 let scope = RekeyScope::Channel(ChannelId(CHAN));
605 let commit = epoch_key_commitment(Epoch(0), &[0xEEu8; 32]);
606 let blob = build_rekey_blob(rotator.secret_key(), &Keys::new(sk(8)).public_key(), scope, Epoch(1), &[0xABu8; 32]).unwrap();
607
608 let outer = build_channel_rekey_event(
609 &Keys::generate(), &rotator, &SR, &ChannelId(CHAN), Epoch(1), Epoch(0), &commit, &[blob.clone()],
610 )
611 .unwrap();
612 assert_ne!(outer.pubkey, rotator.public_key());
614
615 let parsed = open_rekey_event(&outer, &SR).unwrap();
617 assert_eq!(parsed.rotator, rotator.public_key(), "rotator recovered from the inner sig");
618 assert!(matches!(parsed.scope, RekeyScope::Channel(c) if c.0 == CHAN));
619 assert_eq!(parsed.new_epoch, Epoch(1));
620 assert_eq!(parsed.prev_epoch, Epoch(0));
621 assert_eq!(parsed.prev_key_commitment, commit);
622 assert_eq!(parsed.blobs, vec![blob]);
623 }
624
625 #[test]
626 fn epochs_are_independently_recoverable_with_only_the_server_root() {
627 let rotator = Keys::new(sk(1));
631 let recipient = Keys::new(sk(8));
632 let scope = RekeyScope::Channel(ChannelId(CHAN));
633
634 let mut events = Vec::new();
636 let mut keys = std::collections::HashMap::new();
637 for e in 1..=5u64 {
638 let key = [e as u8; 32];
639 keys.insert(e, key);
640 let blob = build_rekey_blob(rotator.secret_key(), &recipient.public_key(), scope, Epoch(e), &key).unwrap();
641 events.push(build_channel_rekey_event(
642 &Keys::generate(), &rotator, &SR, &ChannelId(CHAN), Epoch(e), Epoch(e - 1),
643 &epoch_key_commitment(Epoch(e - 1), &[0u8; 32]), &[blob],
644 ).unwrap());
645 }
646
647 let want = rekey_pseudonym(&ServerRootKey(SR), &ChannelId(CHAN), Epoch(5)).to_hex();
650 let latest = events.iter().find(|ev| ev.tags.iter().any(|t| {
651 let s = t.as_slice();
652 s.len() >= 2 && s[0] == "z" && s[1] == want
653 })).expect("epoch-5 rekey is addressable from the server root");
654 let parsed = open_rekey_event(latest, &SR).unwrap();
655 let secret = rekey_pairwise_secret(recipient.secret_key(), &parsed.rotator).unwrap();
656 let loc = recipient_pseudonym(&secret, parsed.scope, parsed.new_epoch).to_hex();
657 let mine = parsed.blobs.iter().find(|b| b.locator == loc).unwrap();
658 let got = open_rekey_blob(recipient.secret_key(), &parsed.rotator, parsed.scope, parsed.new_epoch, mine).unwrap();
659 assert_eq!(got, keys[&5], "recovered the latest key with only the server root, skipping all earlier epochs");
660 }
661
662 #[test]
663 fn end_to_end_recipient_opens_their_new_key() {
664 let rotator = Keys::new(sk(1));
667 let recipient = Keys::new(sk(8));
668 let scope = RekeyScope::Channel(ChannelId(CHAN));
669 let new_key = [0xCDu8; 32];
670 let blob = build_rekey_blob(rotator.secret_key(), &recipient.public_key(), scope, Epoch(1), &new_key).unwrap();
671 let outer = build_channel_rekey_event(
672 &Keys::generate(), &rotator, &SR, &ChannelId(CHAN), Epoch(1), Epoch(0),
673 &epoch_key_commitment(Epoch(0), &[0u8; 32]), &[blob],
674 )
675 .unwrap();
676
677 let parsed = open_rekey_event(&outer, &SR).unwrap();
678 let secret = rekey_pairwise_secret(recipient.secret_key(), &parsed.rotator).unwrap();
680 let my_locator = recipient_pseudonym(&secret, parsed.scope, parsed.new_epoch).to_hex();
681 let mine = parsed.blobs.iter().find(|b| b.locator == my_locator).expect("my blob is present");
682 let got = open_rekey_blob(recipient.secret_key(), &parsed.rotator, parsed.scope, parsed.new_epoch, mine).unwrap();
683 assert_eq!(got, new_key, "recipient recovers the fresh epoch key end to end");
684 }
685
686 #[test]
687 fn server_root_rekey_round_trips_under_the_prior_root() {
688 let rotator = Keys::new(sk(1));
692 let recipient = Keys::new(sk(8));
693 let prior_root = [0x66u8; 32];
694 let community_id = CommunityId([0x77u8; 32]);
695 let new_root = [0x99u8; 32];
696 let blob = build_rekey_blob(rotator.secret_key(), &recipient.public_key(), RekeyScope::ServerRoot, Epoch(1), &new_root).unwrap();
697 let commit = epoch_key_commitment(Epoch(0), &prior_root);
698 let outer = build_server_root_rekey_event(
699 &Keys::generate(), &rotator, &prior_root, &community_id, Epoch(1), Epoch(0), &commit, &[blob],
700 )
701 .unwrap();
702 assert_ne!(outer.pubkey, rotator.public_key(), "outer is ephemeral, not the rotator");
703
704 let expected = base_rekey_pseudonym(&ServerRootKey(prior_root), &community_id, Epoch(1)).to_hex();
706 let z = outer.tags.iter().find_map(|t| {
707 let s = t.as_slice();
708 (s.len() >= 2 && s[0] == "z").then(|| s[1].clone())
709 });
710 assert_eq!(z.as_deref(), Some(expected.as_str()));
711
712 let parsed = open_rekey_event(&outer, &prior_root).unwrap();
714 assert!(matches!(parsed.scope, RekeyScope::ServerRoot));
715 assert_eq!(parsed.rotator, rotator.public_key());
716 let secret = rekey_pairwise_secret(recipient.secret_key(), &parsed.rotator).unwrap();
717 let loc = recipient_pseudonym(&secret, parsed.scope, parsed.new_epoch).to_hex();
718 let mine = parsed.blobs.iter().find(|b| b.locator == loc).unwrap();
719 let got = open_rekey_blob(recipient.secret_key(), &parsed.rotator, parsed.scope, parsed.new_epoch, mine).unwrap();
720 assert_eq!(got, new_root, "recipient recovers the new server root");
721 }
722
723 #[test]
724 fn base_and_channel_blobs_to_same_recipient_same_epoch_do_not_collide() {
725 let sender = Keys::new(sk(1));
729 let recipient = Keys::new(sk(8));
730 let chan = RekeyScope::Channel(ChannelId([0x42u8; 32]));
731 let base = RekeyScope::ServerRoot;
732 let cb = build_rekey_blob(sender.secret_key(), &recipient.public_key(), chan, Epoch(1), &[0xAAu8; 32]).unwrap();
733 let bb = build_rekey_blob(sender.secret_key(), &recipient.public_key(), base, Epoch(1), &[0xBBu8; 32]).unwrap();
734 assert_ne!(cb.locator, bb.locator, "channel-scope and base-scope blobs must not collide");
735 }
736
737 #[test]
738 fn server_root_rekey_not_openable_without_the_prior_root() {
739 let rotator = Keys::new(sk(1));
740 let recipient = Keys::new(sk(8));
741 let prior_root = [0x66u8; 32];
742 let community_id = CommunityId([0x77u8; 32]);
743 let blob = build_rekey_blob(rotator.secret_key(), &recipient.public_key(), RekeyScope::ServerRoot, Epoch(1), &[0x99u8; 32]).unwrap();
744 let outer = build_server_root_rekey_event(
745 &Keys::generate(), &rotator, &prior_root, &community_id, Epoch(1), Epoch(0),
746 &epoch_key_commitment(Epoch(0), &prior_root), &[blob],
747 )
748 .unwrap();
749 assert!(open_rekey_event(&outer, &[0x00u8; 32]).is_err());
752 }
753
754 #[test]
755 fn outer_address_is_server_root_derived_not_channel_key() {
756 let rotator = Keys::new(sk(1));
759 let outer = build_channel_rekey_event(
760 &Keys::generate(), &rotator, &SR, &ChannelId(CHAN), Epoch(1), Epoch(0), &[0u8; 32], &[],
761 )
762 .unwrap();
763 let expected = rekey_pseudonym(&ServerRootKey(SR), &ChannelId(CHAN), Epoch(1)).to_hex();
764 let z = outer.tags.iter().find_map(|t| {
765 let s = t.as_slice();
766 (s.len() >= 2 && s[0] == "z").then(|| s[1].clone())
767 });
768 assert_eq!(z.as_deref(), Some(expected.as_str()));
769 }
770
771 #[test]
772 fn wrong_server_root_cannot_open() {
773 let rotator = Keys::new(sk(1));
774 let outer = build_channel_rekey_event(
775 &Keys::generate(), &rotator, &SR, &ChannelId(CHAN), Epoch(1), Epoch(0), &[0u8; 32], &[],
776 )
777 .unwrap();
778 assert!(open_rekey_event(&outer, &[0x00u8; 32]).is_err(), "a non-member (wrong server root) can't read it");
779 }
780
781 #[test]
782 fn forged_inner_signature_is_rejected() {
783 let rotator = Keys::new(sk(1));
785 let old_key = SR;
786 let inner = EventBuilder::new(Kind::Custom(event_kind::COMMUNITY_REKEY), "[]")
787 .tags([
788 Tag::custom(TAG_SCOPE, [scope_to_hex(RekeyScope::ServerRoot)]),
789 Tag::custom(TAG_NEW_EPOCH, ["1".to_string()]),
790 Tag::custom(TAG_PREV_EPOCH, ["0".to_string()]),
791 Tag::custom(TAG_PREV_COMMIT, ["00".repeat(32)]),
792 ])
793 .finalize(&rotator)
794 .unwrap();
795 let mut v: serde_json::Value = serde_json::from_str(&inner.as_json()).unwrap();
796 v["content"] = serde_json::Value::String("[{\"locator\":\"x\",\"wrapped\":\"y\"}]".into());
797 let tampered = serde_json::to_string(&v).unwrap();
798 let content = cipher::seal(&old_key, tampered.as_bytes()).unwrap();
799 let outer = EventBuilder::new(Kind::Custom(event_kind::COMMUNITY_REKEY), content)
800 .tags([Tag::custom(TAG_VERSION, [PROTOCOL_VERSION.to_string()])])
801 .finalize(&Keys::generate())
802 .unwrap();
803 assert!(open_rekey_event(&outer, &old_key).is_err());
804 }
805
806 #[test]
807 fn wrong_outer_kind_and_bad_version_rejected() {
808 let old_key = [0x55u8; 32];
809 let not_rekey = EventBuilder::new(Kind::Custom(event_kind::COMMUNITY_MESSAGE), "x")
811 .finalize(&Keys::generate())
812 .unwrap();
813 assert!(open_rekey_event(¬_rekey, &old_key).is_err());
814 let bad_ver = EventBuilder::new(Kind::Custom(event_kind::COMMUNITY_REKEY), "x")
816 .tags([Tag::custom(TAG_VERSION, ["999".to_string()])])
817 .finalize(&Keys::generate())
818 .unwrap();
819 assert!(open_rekey_event(&bad_ver, &old_key).is_err());
820 }
821
822 #[test]
823 fn version_is_checked_before_decrypt() {
824 let real_key = [0x55u8; 32];
829 let inner = EventBuilder::new(Kind::Custom(event_kind::COMMUNITY_REKEY), "[]")
830 .finalize(&Keys::generate())
831 .unwrap();
832 let content = cipher::seal(&real_key, inner.as_json().as_bytes()).unwrap();
833 let outer = EventBuilder::new(Kind::Custom(event_kind::COMMUNITY_REKEY), content)
834 .tags([Tag::custom(TAG_VERSION, ["999".to_string()])])
835 .finalize(&Keys::generate())
836 .unwrap();
837 let err = open_rekey_event(&outer, &[0x00u8; 32]).unwrap_err();
840 assert!(err.contains("version"), "must reject on version before decrypt, got: {err}");
841 }
842
843 const GOLDEN_EPOCH_COMMITMENT: &str =
844 "5e706f60f1c6f39208071e914d3284dab5f93a1c8d178260e7daf5d23e26a81f";
845
846 #[test]
847 fn distinct_recipients_get_distinct_locators() {
848 let sender = Keys::new(sk(7));
850 let r1 = Keys::new(sk(8));
851 let r2 = Keys::new(sk(9));
852 let b1 = build_rekey_blob(sender.secret_key(), &r1.public_key(), RekeyScope::ServerRoot, Epoch(1), &[1u8; 32]).unwrap();
853 let b2 = build_rekey_blob(sender.secret_key(), &r2.public_key(), RekeyScope::ServerRoot, Epoch(1), &[1u8; 32]).unwrap();
854 assert_ne!(b1.locator, b2.locator);
855 }
856}