1use nostr_sdk::nips::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(TagKind::Custom(TAG_SCOPE.into()), [scope_to_hex(scope)]),
219 Tag::custom(TagKind::Custom(TAG_NEW_EPOCH.into()), [new_epoch.0.to_string()]),
220 Tag::custom(TagKind::Custom(TAG_PREV_EPOCH.into()), [prev_epoch.0.to_string()]),
221 Tag::custom(TagKind::Custom(TAG_PREV_COMMIT.into()), [crate::simd::hex::bytes_to_hex_32(prev_key_commitment)]),
222 ])
223 .sign_with_keys(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(TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::Z)), [address.to_hex()]),
235 Tag::custom(TagKind::Custom(TAG_VERSION.into()), [PROTOCOL_VERSION.to_string()]),
236 ])
237 .sign_with_keys(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 use nostr_sdk::JsonUtil;
380 let rotator = Keys::generate();
381 let blobs: Vec<RekeyBlob> = (0..MAX_REKEY_BLOBS)
382 .map(|_| {
383 let r = Keys::generate();
384 build_rekey_blob(rotator.secret_key(), &r.public_key(), RekeyScope::ServerRoot, Epoch(1), &[0xCDu8; 32]).unwrap()
385 })
386 .collect();
387 let outer = build_server_root_rekey_event(
388 &Keys::generate(), &rotator, &[0x07u8; 32], &CommunityId([0x09u8; 32]), Epoch(1), Epoch(0), &[0u8; 32], &blobs,
389 )
390 .unwrap();
391 let size = outer.as_json().len();
392 assert!(
393 size <= STRFRY_MAX_EVENT_SIZE,
394 "a full {MAX_REKEY_BLOBS}-blob rekey event is {size} bytes; must stay <= {STRFRY_MAX_EVENT_SIZE} (strfry maxEventSize)"
395 );
396 }
397
398 fn sk(byte: u8) -> SecretKey {
399 SecretKey::from_slice(&[byte; 32]).unwrap()
400 }
401
402 #[test]
403 fn bound_plaintext_layout_is_frozen() {
404 let pt = bound_plaintext(RekeyScope::ServerRoot, Epoch(1), &[0xABu8; 32]);
407 let expected = format!("{}{}{}", "00".repeat(32), "0000000000000001", "ab".repeat(32));
408 assert_eq!(crate::simd::hex::bytes_to_hex_string(&pt), expected);
409 let pt2 = bound_plaintext(RekeyScope::Channel(ChannelId([0x11u8; 32])), Epoch(0x0102), &[0xCDu8; 32]);
411 let expected2 = format!("{}{}{}", "11".repeat(32), "0000000000000102", "cd".repeat(32));
412 assert_eq!(crate::simd::hex::bytes_to_hex_string(&pt2), expected2);
413 }
414
415 #[test]
416 fn pairwise_secret_regression_pin() {
417 let a = Keys::new(sk(1));
422 let b = Keys::new(sk(2));
423 let secret = rekey_pairwise_secret(a.secret_key(), &b.public_key()).unwrap();
424 assert_eq!(crate::simd::hex::bytes_to_hex_string(&secret), GOLDEN_PAIRWISE_SECRET);
425 }
426
427 const GOLDEN_PAIRWISE_SECRET: &str =
430 "59c6d24d9c3a7bf8ca4cec54031a3e2ecfaa553452a2b2fa3147e31ee55f33d5";
431
432 #[test]
433 fn pairwise_secret_is_symmetric_and_deterministic() {
434 let a = Keys::new(sk(1));
435 let b = Keys::new(sk(2));
436 let from_a = rekey_pairwise_secret(a.secret_key(), &b.public_key()).unwrap();
437 let from_b = rekey_pairwise_secret(b.secret_key(), &a.public_key()).unwrap();
438 assert_eq!(from_a, from_b, "ECDH is symmetric: both sides derive the same secret");
439 assert_eq!(from_a, rekey_pairwise_secret(a.secret_key(), &b.public_key()).unwrap());
441 let c = Keys::new(sk(3));
443 assert_ne!(from_a, rekey_pairwise_secret(a.secret_key(), &c.public_key()).unwrap());
444 }
445
446 #[test]
447 fn blob_round_trips_server_root_scope() {
448 let sender = Keys::new(sk(7));
449 let recipient = Keys::new(sk(8));
450 let new_key = [0xABu8; 32];
451 let blob = build_rekey_blob(
452 sender.secret_key(), &recipient.public_key(), RekeyScope::ServerRoot, Epoch(1), &new_key,
453 )
454 .unwrap();
455 let got = open_rekey_blob(
456 recipient.secret_key(), &sender.public_key(), RekeyScope::ServerRoot, Epoch(1), &blob,
457 )
458 .unwrap();
459 assert_eq!(got, new_key, "the recipient recovers the fresh key");
460 }
461
462 #[test]
463 fn blob_round_trips_channel_scope() {
464 let sender = Keys::new(sk(7));
465 let recipient = Keys::new(sk(8));
466 let chan = RekeyScope::Channel(ChannelId([0x42u8; 32]));
467 let new_key = [0xCDu8; 32];
468 let blob = build_rekey_blob(sender.secret_key(), &recipient.public_key(), chan, Epoch(5), &new_key).unwrap();
469 let got = open_rekey_blob(recipient.secret_key(), &sender.public_key(), chan, Epoch(5), &blob).unwrap();
470 assert_eq!(got, new_key);
471 }
472
473 #[test]
474 fn locator_is_the_recipient_pseudonym() {
475 let sender = Keys::new(sk(7));
478 let recipient = Keys::new(sk(8));
479 let secret = rekey_pairwise_secret(sender.secret_key(), &recipient.public_key()).unwrap();
480 let blob = build_rekey_blob(
481 sender.secret_key(), &recipient.public_key(), RekeyScope::ServerRoot, Epoch(2), &[1u8; 32],
482 )
483 .unwrap();
484 assert_eq!(blob.locator, recipient_pseudonym(&secret, RekeyScope::ServerRoot, Epoch(2)).to_hex());
485 }
486
487 #[test]
488 fn wrong_sender_cannot_open() {
489 let sender = Keys::new(sk(7));
492 let recipient = Keys::new(sk(8));
493 let impostor = Keys::new(sk(9));
494 let blob = build_rekey_blob(
495 sender.secret_key(), &recipient.public_key(), RekeyScope::ServerRoot, Epoch(1), &[2u8; 32],
496 )
497 .unwrap();
498 let err = open_rekey_blob(
499 recipient.secret_key(), &impostor.public_key(), RekeyScope::ServerRoot, Epoch(1), &blob,
500 );
501 assert!(err.is_err(), "pairing against the wrong sender must fail");
502 }
503
504 #[test]
505 fn non_recipient_cannot_open() {
506 let sender = Keys::new(sk(7));
509 let recipient = Keys::new(sk(8));
510 let other = Keys::new(sk(10));
511 let blob = build_rekey_blob(
512 sender.secret_key(), &recipient.public_key(), RekeyScope::ServerRoot, Epoch(1), &[3u8; 32],
513 )
514 .unwrap();
515 assert!(open_rekey_blob(
516 other.secret_key(), &sender.public_key(), RekeyScope::ServerRoot, Epoch(1), &blob,
517 )
518 .is_err());
519 }
520
521 #[test]
522 fn scope_splice_is_rejected() {
523 let sender = Keys::new(sk(7));
526 let recipient = Keys::new(sk(8));
527 let blob = build_rekey_blob(
528 sender.secret_key(), &recipient.public_key(), RekeyScope::ServerRoot, Epoch(1), &[4u8; 32],
529 )
530 .unwrap();
531 let chan = RekeyScope::Channel(ChannelId([0x42u8; 32]));
532 assert!(open_rekey_blob(recipient.secret_key(), &sender.public_key(), chan, Epoch(1), &blob).is_err());
533 }
534
535 #[test]
536 fn epoch_splice_is_rejected() {
537 let sender = Keys::new(sk(7));
538 let recipient = Keys::new(sk(8));
539 let blob = build_rekey_blob(
540 sender.secret_key(), &recipient.public_key(), RekeyScope::ServerRoot, Epoch(1), &[5u8; 32],
541 )
542 .unwrap();
543 assert!(open_rekey_blob(
544 recipient.secret_key(), &sender.public_key(), RekeyScope::ServerRoot, Epoch(2), &blob,
545 )
546 .is_err());
547 }
548
549 #[test]
550 fn relocated_blob_is_rejected() {
551 let sender = Keys::new(sk(7));
554 let recipient = Keys::new(sk(8));
555 let mut blob = build_rekey_blob(
556 sender.secret_key(), &recipient.public_key(), RekeyScope::ServerRoot, Epoch(1), &[6u8; 32],
557 )
558 .unwrap();
559 blob.locator = "ff".repeat(32);
560 assert!(open_rekey_blob(
561 recipient.secret_key(), &sender.public_key(), RekeyScope::ServerRoot, Epoch(1), &blob,
562 )
563 .is_err());
564 }
565
566 #[test]
567 fn tampered_ciphertext_is_rejected() {
568 let sender = Keys::new(sk(7));
569 let recipient = Keys::new(sk(8));
570 let mut blob = build_rekey_blob(
571 sender.secret_key(), &recipient.public_key(), RekeyScope::ServerRoot, Epoch(1), &[7u8; 32],
572 )
573 .unwrap();
574 let mut bytes = base64_simd::STANDARD.decode_to_vec(blob.wrapped.as_bytes()).unwrap();
576 let mid = bytes.len() / 2;
577 bytes[mid] ^= 0xff;
578 blob.wrapped = base64_simd::STANDARD.encode_to_string(&bytes);
579 assert!(open_rekey_blob(
580 recipient.secret_key(), &sender.public_key(), RekeyScope::ServerRoot, Epoch(1), &blob,
581 )
582 .is_err());
583 }
584
585 const SR: [u8; 32] = [0x55u8; 32];
589 const CHAN: [u8; 32] = [0x42u8; 32];
590
591 #[test]
592 fn epoch_key_commitment_golden_and_binds_epoch() {
593 let c = epoch_key_commitment(Epoch(1), &[0x33u8; 32]);
595 assert_eq!(crate::simd::hex::bytes_to_hex_32(&c), GOLDEN_EPOCH_COMMITMENT);
596 assert_ne!(c, epoch_key_commitment(Epoch(2), &[0x33u8; 32]));
598 assert_ne!(c, epoch_key_commitment(Epoch(1), &[0x34u8; 32]));
600 }
601
602 #[test]
603 fn channel_rekey_round_trips() {
604 let rotator = Keys::new(sk(1));
605 let scope = RekeyScope::Channel(ChannelId(CHAN));
606 let commit = epoch_key_commitment(Epoch(0), &[0xEEu8; 32]);
607 let blob = build_rekey_blob(rotator.secret_key(), &Keys::new(sk(8)).public_key(), scope, Epoch(1), &[0xABu8; 32]).unwrap();
608
609 let outer = build_channel_rekey_event(
610 &Keys::generate(), &rotator, &SR, &ChannelId(CHAN), Epoch(1), Epoch(0), &commit, &[blob.clone()],
611 )
612 .unwrap();
613 assert_ne!(outer.pubkey, rotator.public_key());
615
616 let parsed = open_rekey_event(&outer, &SR).unwrap();
618 assert_eq!(parsed.rotator, rotator.public_key(), "rotator recovered from the inner sig");
619 assert!(matches!(parsed.scope, RekeyScope::Channel(c) if c.0 == CHAN));
620 assert_eq!(parsed.new_epoch, Epoch(1));
621 assert_eq!(parsed.prev_epoch, Epoch(0));
622 assert_eq!(parsed.prev_key_commitment, commit);
623 assert_eq!(parsed.blobs, vec![blob]);
624 }
625
626 #[test]
627 fn epochs_are_independently_recoverable_with_only_the_server_root() {
628 let rotator = Keys::new(sk(1));
632 let recipient = Keys::new(sk(8));
633 let scope = RekeyScope::Channel(ChannelId(CHAN));
634
635 let mut events = Vec::new();
637 let mut keys = std::collections::HashMap::new();
638 for e in 1..=5u64 {
639 let key = [e as u8; 32];
640 keys.insert(e, key);
641 let blob = build_rekey_blob(rotator.secret_key(), &recipient.public_key(), scope, Epoch(e), &key).unwrap();
642 events.push(build_channel_rekey_event(
643 &Keys::generate(), &rotator, &SR, &ChannelId(CHAN), Epoch(e), Epoch(e - 1),
644 &epoch_key_commitment(Epoch(e - 1), &[0u8; 32]), &[blob],
645 ).unwrap());
646 }
647
648 let want = rekey_pseudonym(&ServerRootKey(SR), &ChannelId(CHAN), Epoch(5)).to_hex();
651 let latest = events.iter().find(|ev| ev.tags.iter().any(|t| {
652 let s = t.as_slice();
653 s.len() >= 2 && s[0] == "z" && s[1] == want
654 })).expect("epoch-5 rekey is addressable from the server root");
655 let parsed = open_rekey_event(latest, &SR).unwrap();
656 let secret = rekey_pairwise_secret(recipient.secret_key(), &parsed.rotator).unwrap();
657 let loc = recipient_pseudonym(&secret, parsed.scope, parsed.new_epoch).to_hex();
658 let mine = parsed.blobs.iter().find(|b| b.locator == loc).unwrap();
659 let got = open_rekey_blob(recipient.secret_key(), &parsed.rotator, parsed.scope, parsed.new_epoch, mine).unwrap();
660 assert_eq!(got, keys[&5], "recovered the latest key with only the server root, skipping all earlier epochs");
661 }
662
663 #[test]
664 fn end_to_end_recipient_opens_their_new_key() {
665 let rotator = Keys::new(sk(1));
668 let recipient = Keys::new(sk(8));
669 let scope = RekeyScope::Channel(ChannelId(CHAN));
670 let new_key = [0xCDu8; 32];
671 let blob = build_rekey_blob(rotator.secret_key(), &recipient.public_key(), scope, Epoch(1), &new_key).unwrap();
672 let outer = build_channel_rekey_event(
673 &Keys::generate(), &rotator, &SR, &ChannelId(CHAN), Epoch(1), Epoch(0),
674 &epoch_key_commitment(Epoch(0), &[0u8; 32]), &[blob],
675 )
676 .unwrap();
677
678 let parsed = open_rekey_event(&outer, &SR).unwrap();
679 let secret = rekey_pairwise_secret(recipient.secret_key(), &parsed.rotator).unwrap();
681 let my_locator = recipient_pseudonym(&secret, parsed.scope, parsed.new_epoch).to_hex();
682 let mine = parsed.blobs.iter().find(|b| b.locator == my_locator).expect("my blob is present");
683 let got = open_rekey_blob(recipient.secret_key(), &parsed.rotator, parsed.scope, parsed.new_epoch, mine).unwrap();
684 assert_eq!(got, new_key, "recipient recovers the fresh epoch key end to end");
685 }
686
687 #[test]
688 fn server_root_rekey_round_trips_under_the_prior_root() {
689 let rotator = Keys::new(sk(1));
693 let recipient = Keys::new(sk(8));
694 let prior_root = [0x66u8; 32];
695 let community_id = CommunityId([0x77u8; 32]);
696 let new_root = [0x99u8; 32];
697 let blob = build_rekey_blob(rotator.secret_key(), &recipient.public_key(), RekeyScope::ServerRoot, Epoch(1), &new_root).unwrap();
698 let commit = epoch_key_commitment(Epoch(0), &prior_root);
699 let outer = build_server_root_rekey_event(
700 &Keys::generate(), &rotator, &prior_root, &community_id, Epoch(1), Epoch(0), &commit, &[blob],
701 )
702 .unwrap();
703 assert_ne!(outer.pubkey, rotator.public_key(), "outer is ephemeral, not the rotator");
704
705 let expected = base_rekey_pseudonym(&ServerRootKey(prior_root), &community_id, Epoch(1)).to_hex();
707 let z = outer.tags.iter().find_map(|t| {
708 let s = t.as_slice();
709 (s.len() >= 2 && s[0] == "z").then(|| s[1].clone())
710 });
711 assert_eq!(z.as_deref(), Some(expected.as_str()));
712
713 let parsed = open_rekey_event(&outer, &prior_root).unwrap();
715 assert!(matches!(parsed.scope, RekeyScope::ServerRoot));
716 assert_eq!(parsed.rotator, rotator.public_key());
717 let secret = rekey_pairwise_secret(recipient.secret_key(), &parsed.rotator).unwrap();
718 let loc = recipient_pseudonym(&secret, parsed.scope, parsed.new_epoch).to_hex();
719 let mine = parsed.blobs.iter().find(|b| b.locator == loc).unwrap();
720 let got = open_rekey_blob(recipient.secret_key(), &parsed.rotator, parsed.scope, parsed.new_epoch, mine).unwrap();
721 assert_eq!(got, new_root, "recipient recovers the new server root");
722 }
723
724 #[test]
725 fn base_and_channel_blobs_to_same_recipient_same_epoch_do_not_collide() {
726 let sender = Keys::new(sk(1));
730 let recipient = Keys::new(sk(8));
731 let chan = RekeyScope::Channel(ChannelId([0x42u8; 32]));
732 let base = RekeyScope::ServerRoot;
733 let cb = build_rekey_blob(sender.secret_key(), &recipient.public_key(), chan, Epoch(1), &[0xAAu8; 32]).unwrap();
734 let bb = build_rekey_blob(sender.secret_key(), &recipient.public_key(), base, Epoch(1), &[0xBBu8; 32]).unwrap();
735 assert_ne!(cb.locator, bb.locator, "channel-scope and base-scope blobs must not collide");
736 }
737
738 #[test]
739 fn server_root_rekey_not_openable_without_the_prior_root() {
740 let rotator = Keys::new(sk(1));
741 let recipient = Keys::new(sk(8));
742 let prior_root = [0x66u8; 32];
743 let community_id = CommunityId([0x77u8; 32]);
744 let blob = build_rekey_blob(rotator.secret_key(), &recipient.public_key(), RekeyScope::ServerRoot, Epoch(1), &[0x99u8; 32]).unwrap();
745 let outer = build_server_root_rekey_event(
746 &Keys::generate(), &rotator, &prior_root, &community_id, Epoch(1), Epoch(0),
747 &epoch_key_commitment(Epoch(0), &prior_root), &[blob],
748 )
749 .unwrap();
750 assert!(open_rekey_event(&outer, &[0x00u8; 32]).is_err());
753 }
754
755 #[test]
756 fn outer_address_is_server_root_derived_not_channel_key() {
757 let rotator = Keys::new(sk(1));
760 let outer = build_channel_rekey_event(
761 &Keys::generate(), &rotator, &SR, &ChannelId(CHAN), Epoch(1), Epoch(0), &[0u8; 32], &[],
762 )
763 .unwrap();
764 let expected = rekey_pseudonym(&ServerRootKey(SR), &ChannelId(CHAN), Epoch(1)).to_hex();
765 let z = outer.tags.iter().find_map(|t| {
766 let s = t.as_slice();
767 (s.len() >= 2 && s[0] == "z").then(|| s[1].clone())
768 });
769 assert_eq!(z.as_deref(), Some(expected.as_str()));
770 }
771
772 #[test]
773 fn wrong_server_root_cannot_open() {
774 let rotator = Keys::new(sk(1));
775 let outer = build_channel_rekey_event(
776 &Keys::generate(), &rotator, &SR, &ChannelId(CHAN), Epoch(1), Epoch(0), &[0u8; 32], &[],
777 )
778 .unwrap();
779 assert!(open_rekey_event(&outer, &[0x00u8; 32]).is_err(), "a non-member (wrong server root) can't read it");
780 }
781
782 #[test]
783 fn forged_inner_signature_is_rejected() {
784 let rotator = Keys::new(sk(1));
786 let old_key = SR;
787 let inner = EventBuilder::new(Kind::Custom(event_kind::COMMUNITY_REKEY), "[]")
788 .tags([
789 Tag::custom(TagKind::Custom(TAG_SCOPE.into()), [scope_to_hex(RekeyScope::ServerRoot)]),
790 Tag::custom(TagKind::Custom(TAG_NEW_EPOCH.into()), ["1".to_string()]),
791 Tag::custom(TagKind::Custom(TAG_PREV_EPOCH.into()), ["0".to_string()]),
792 Tag::custom(TagKind::Custom(TAG_PREV_COMMIT.into()), ["00".repeat(32)]),
793 ])
794 .sign_with_keys(&rotator)
795 .unwrap();
796 let mut v: serde_json::Value = serde_json::from_str(&inner.as_json()).unwrap();
797 v["content"] = serde_json::Value::String("[{\"locator\":\"x\",\"wrapped\":\"y\"}]".into());
798 let tampered = serde_json::to_string(&v).unwrap();
799 let content = cipher::seal(&old_key, tampered.as_bytes()).unwrap();
800 let outer = EventBuilder::new(Kind::Custom(event_kind::COMMUNITY_REKEY), content)
801 .tags([Tag::custom(TagKind::Custom(TAG_VERSION.into()), [PROTOCOL_VERSION.to_string()])])
802 .sign_with_keys(&Keys::generate())
803 .unwrap();
804 assert!(open_rekey_event(&outer, &old_key).is_err());
805 }
806
807 #[test]
808 fn wrong_outer_kind_and_bad_version_rejected() {
809 let old_key = [0x55u8; 32];
810 let not_rekey = EventBuilder::new(Kind::Custom(event_kind::COMMUNITY_MESSAGE), "x")
812 .sign_with_keys(&Keys::generate())
813 .unwrap();
814 assert!(open_rekey_event(¬_rekey, &old_key).is_err());
815 let bad_ver = EventBuilder::new(Kind::Custom(event_kind::COMMUNITY_REKEY), "x")
817 .tags([Tag::custom(TagKind::Custom(TAG_VERSION.into()), ["999".to_string()])])
818 .sign_with_keys(&Keys::generate())
819 .unwrap();
820 assert!(open_rekey_event(&bad_ver, &old_key).is_err());
821 }
822
823 #[test]
824 fn version_is_checked_before_decrypt() {
825 let real_key = [0x55u8; 32];
830 let inner = EventBuilder::new(Kind::Custom(event_kind::COMMUNITY_REKEY), "[]")
831 .sign_with_keys(&Keys::generate())
832 .unwrap();
833 let content = cipher::seal(&real_key, inner.as_json().as_bytes()).unwrap();
834 let outer = EventBuilder::new(Kind::Custom(event_kind::COMMUNITY_REKEY), content)
835 .tags([Tag::custom(TagKind::Custom(TAG_VERSION.into()), ["999".to_string()])])
836 .sign_with_keys(&Keys::generate())
837 .unwrap();
838 let err = open_rekey_event(&outer, &[0x00u8; 32]).unwrap_err();
841 assert!(err.contains("version"), "must reject on version before decrypt, got: {err}");
842 }
843
844 const GOLDEN_EPOCH_COMMITMENT: &str =
845 "5e706f60f1c6f39208071e914d3284dab5f93a1c8d178260e7daf5d23e26a81f";
846
847 #[test]
848 fn distinct_recipients_get_distinct_locators() {
849 let sender = Keys::new(sk(7));
851 let r1 = Keys::new(sk(8));
852 let r2 = Keys::new(sk(9));
853 let b1 = build_rekey_blob(sender.secret_key(), &r1.public_key(), RekeyScope::ServerRoot, Epoch(1), &[1u8; 32]).unwrap();
854 let b2 = build_rekey_blob(sender.secret_key(), &r2.public_key(), RekeyScope::ServerRoot, Epoch(1), &[1u8; 32]).unwrap();
855 assert_ne!(b1.locator, b2.locator);
856 }
857}