onionwire/tests/rotate.rs
Sirius DevOps 07aa97711c
[verified] Add F4 onion locator rotate with signed loc frames.
Identity key stays put. Typing ROTATE publishes a new Arti HS, hard-cuts
the old nickname, persists self.onion, and pushes loc to reachable friends.
2026-09-10 02:43:45 -04:00

132 lines
4.1 KiB
Rust

//! M4: loc frames, F4 confirm typing, identity stays put.
use ed25519_dalek::SigningKey;
use onionwire::Store;
use onionwire::loc::{self, Loc};
use onionwire::tui::{RotateDecision, RotatePrompt};
use rand::rngs::OsRng;
fn pk(tag: u8) -> [u8; 32] {
let mut k = [0u8; 32];
k[0] = tag;
k
}
fn store() -> (tempfile::TempDir, Store) {
let dir = tempfile::tempdir().expect("tempdir");
let store = Store::open_at(dir.path()).expect("open");
(dir, store)
}
#[test]
fn signed_loc_updates_friend_onion_keeps_fingerprint() {
let (_dir, store) = store();
let sk = SigningKey::generate(&mut OsRng);
let pk = sk.verifying_key().to_bytes();
store
.upsert_friend(&pk, "old.onion", Some("alice"))
.unwrap();
store.set_friend_prekey(&pk, &[1u8; 32]).unwrap();
let before = store.get_friend(&pk).unwrap().unwrap();
let ts = 2_000_000_000;
let loc = loc::sign(&sk.to_bytes(), "new.onion", ts).expect("sign");
assert!(
store.apply_loc(&pk, &loc.onion, loc.ts, &loc.sig).unwrap(),
"newer signed loc must apply"
);
let after = store.get_friend(&pk).unwrap().unwrap();
assert_eq!(after.onion, "new.onion");
assert_eq!(after.fingerprint, before.fingerprint);
assert_eq!(after.petname.as_deref(), Some("alice"));
assert_eq!(store.friend_count().unwrap(), 1);
}
#[test]
fn unsigned_loc_does_not_change_row() {
let (_dir, store) = store();
store.upsert_friend(&pk(1), "old.onion", Some("c")).unwrap();
let loc = Loc {
onion: "evil.onion".into(),
ts: 2_000_000_000,
sig: vec![],
};
assert!(
!store
.apply_loc(&pk(1), &loc.onion, loc.ts, &loc.sig)
.unwrap()
);
assert_eq!(
store.get_friend(&pk(1)).unwrap().unwrap().onion,
"old.onion"
);
}
#[test]
fn wrong_key_loc_does_not_change_row() {
let (_dir, store) = store();
let owner = SigningKey::generate(&mut OsRng);
let owner_pk = owner.verifying_key().to_bytes();
store.upsert_friend(&owner_pk, "old.onion", None).unwrap();
let other = SigningKey::generate(&mut OsRng);
let loc = loc::sign(&other.to_bytes(), "hijack.onion", 2_000_000_000).unwrap();
assert!(
!store
.apply_loc(&owner_pk, &loc.onion, loc.ts, &loc.sig)
.unwrap()
);
assert_eq!(
store.get_friend(&owner_pk).unwrap().unwrap().onion,
"old.onion"
);
}
#[test]
fn stale_ts_loc_does_not_change_row() {
let (_dir, store) = store();
let sk = SigningKey::generate(&mut OsRng);
let pk = sk.verifying_key().to_bytes();
store.upsert_friend(&pk, "old.onion", None).unwrap();
let loc = loc::sign(&sk.to_bytes(), "new.onion", 1).unwrap();
assert!(!store.apply_loc(&pk, &loc.onion, loc.ts, &loc.sig).unwrap());
assert_eq!(store.get_friend(&pk).unwrap().unwrap().onion, "old.onion");
}
#[test]
fn loc_frame_roundtrip_and_chat_is_not_loc() {
let loc = loc::sign(&[7u8; 32], "abc.onion", 42).unwrap();
let bytes = loc::encode(&loc);
let parsed = loc::decode(&bytes).expect("loc frame");
assert_eq!(parsed.onion, "abc.onion");
assert_eq!(parsed.ts, 42);
assert_eq!(parsed.sig, loc.sig);
assert!(loc::decode(b"hello wire").is_none());
}
#[test]
fn f4_requires_typing_rotate_not_enter() {
let mut p = RotatePrompt::new();
assert_eq!(p.on_esc(), RotateDecision::Cancel);
assert_eq!(
p.on_char('\n'),
RotateDecision::Pending,
"Enter must not rotate"
);
for c in "ROTA".chars() {
assert_eq!(p.on_char(c), RotateDecision::Pending);
}
assert_eq!(p.on_char('T'), RotateDecision::Pending);
assert_eq!(p.on_char('E'), RotateDecision::Confirm);
}
#[test]
fn rotate_prompt_text_matches_spec() {
let t = onionwire::tui::rotate_screen_text();
assert!(t.contains("Rotate onion address?"));
assert!(t.contains("Your identity key stays the same."));
assert!(t.contains("Online friends get a signed location update."));
assert!(t.contains("Offline friends CANNOT find you until they rescan your QR."));
assert!(t.contains("Type ROTATE to confirm"));
assert!(t.contains("Esc to cancel"));
}