use dosh::auth::{ build_bootstrap, decode_bootstrap, encode_bootstrap, load_or_create_server_secret, open_attach_ticket, verify_attach_ticket, verify_bootstrap, }; use dosh::config::ServerConfig; use dosh::crypto; use dosh::protocol::{self, CLIENT_TO_SERVER, PacketKind, ReplayWindow}; #[test] fn bootstrap_round_trips_and_verifies() { let dir = tempfile::tempdir().unwrap(); let config = ServerConfig { secret_path: dir.path().join("secret").display().to_string(), ..ServerConfig::default() }; let secret = load_or_create_server_secret(&config).unwrap(); let nonce = crypto::random_12(); let resp = build_bootstrap( &config, &secret, "user".to_string(), "default".to_string(), "read-write".to_string(), (80, 24), nonce, "127.0.0.1".to_string(), ) .unwrap(); assert!(verify_bootstrap(&resp, &secret).unwrap()); let encoded = encode_bootstrap(&resp).unwrap(); let decoded = decode_bootstrap(&encoded).unwrap(); assert_eq!(decoded.session, "default"); assert_eq!(decoded.session_key, resp.session_key); } #[test] fn encrypted_packet_round_trips() { let key = crypto::random_32(); let conn_id = crypto::random_16(); let packet = protocol::encode_encrypted( PacketKind::Input, conn_id, 7, 0, &key, CLIENT_TO_SERVER, b"hello", ) .unwrap(); let decoded = protocol::decode(&packet).unwrap(); assert_eq!(decoded.header.kind, PacketKind::Input); assert_eq!(decoded.header.conn_id, conn_id); assert_eq!( decoded.header.session_key_id, protocol::session_key_id(&key) ); let plain = protocol::decrypt_body(&decoded, &key, CLIENT_TO_SERVER).unwrap(); assert_eq!(plain, b"hello"); } #[test] fn encrypted_packet_rejects_wrong_session_key_id_before_decrypt() { let key = crypto::random_32(); let wrong_key = crypto::random_32(); let packet = protocol::encode_encrypted( PacketKind::Input, crypto::random_16(), 1, 0, &key, CLIENT_TO_SERVER, b"hello", ) .unwrap(); let decoded = protocol::decode(&packet).unwrap(); let err = protocol::decrypt_body(&decoded, &wrong_key, CLIENT_TO_SERVER).unwrap_err(); assert!(err.to_string().contains("session key id")); } #[test] fn attach_ticket_is_sealed_and_verifies_scope() { let dir = tempfile::tempdir().unwrap(); let config = ServerConfig { secret_path: dir.path().join("secret").display().to_string(), ..ServerConfig::default() }; let secret = load_or_create_server_secret(&config).unwrap(); let resp = build_bootstrap( &config, &secret, "user".to_string(), "work".to_string(), "view-only".to_string(), (100, 30), crypto::random_12(), "127.0.0.1".to_string(), ) .unwrap(); let opened = open_attach_ticket(&secret, &resp.attach_ticket).unwrap(); assert_eq!(opened.session, "work"); assert_eq!(opened.mode, "view-only"); assert_eq!(opened.psk, resp.attach_ticket_psk); assert!( verify_attach_ticket( &secret, &resp.attach_ticket, &resp.attach_ticket_psk, "work", "view-only", ) .unwrap() .is_some() ); assert!( verify_attach_ticket( &secret, &resp.attach_ticket, &resp.attach_ticket_psk, "default", "view-only", ) .unwrap() .is_none() ); } #[test] fn peek_foreign_wire_version_flags_only_version_skew() { let key = crypto::random_32(); let mut packet = protocol::encode_encrypted( PacketKind::Input, crypto::random_16(), 1, 0, &key, CLIENT_TO_SERVER, b"hi", ) .unwrap(); // A correctly framed packet for this build is not "foreign". assert_eq!(protocol::peek_foreign_wire_version(&packet), None); // Bumping the wire version byte makes it undecodable but recognizable. packet[4] = protocol::VERSION.wrapping_add(7); assert_eq!( protocol::peek_foreign_wire_version(&packet), Some(protocol::VERSION.wrapping_add(7)) ); assert!(protocol::decode(&packet).is_err()); // Non-Dosh datagrams and runts are ignored. assert_eq!(protocol::peek_foreign_wire_version(b"XXXX\x01"), None); assert_eq!(protocol::peek_foreign_wire_version(b"DOS"), None); } #[test] fn rekey_key_derivation_agrees_and_is_independent_per_epoch() { use dosh::native::derive_rekey_session_key; // The handshake/current key both peers already share. let current_key = crypto::random_32(); let current_id = protocol::session_key_id(¤t_key); // Fresh server-generated material, delivered confidentially in the Rekey. let material = crypto::random_32(); // Both peers derive identically from shared current key + shipped material. let server_view = derive_rekey_session_key(¤t_key, &material, ¤t_id, 1).unwrap(); let client_view = derive_rekey_session_key(¤t_key, &material, ¤t_id, 1).unwrap(); assert_eq!(server_view, client_view); // The rotated key must not equal the handshake/current key. assert_ne!(server_view, current_key); // A different epoch (or different material) yields a different key. let next_epoch = derive_rekey_session_key(¤t_key, &material, ¤t_id, 2).unwrap(); assert_ne!(server_view, next_epoch); let other_material = crypto::random_32(); let other = derive_rekey_session_key(¤t_key, &other_material, ¤t_id, 1).unwrap(); assert_ne!(server_view, other); } #[test] fn rekey_round_trip_decrypts_old_and_new_epoch_packets() { use dosh::native::derive_rekey_session_key; let key_epoch0 = crypto::random_32(); let id0 = protocol::session_key_id(&key_epoch0); let conn_id = crypto::random_16(); // Pre-rekey packet sealed under epoch-0 key. let pre = protocol::encode_encrypted( PacketKind::Frame, conn_id, 5, 0, &key_epoch0, protocol::SERVER_TO_CLIENT, b"before", ) .unwrap(); // Rotate to epoch 1. let material = crypto::random_32(); let key_epoch1 = derive_rekey_session_key(&key_epoch0, &material, &id0, 1).unwrap(); let post = protocol::encode_encrypted( PacketKind::Frame, conn_id, 6, 0, &key_epoch1, protocol::SERVER_TO_CLIENT, b"after", ) .unwrap(); let pre = protocol::decode(&pre).unwrap(); let post = protocol::decode(&post).unwrap(); // Each epoch's key carries its own session_key_id; the receiver picks the // right one and both decrypt correctly. assert_eq!(pre.header.session_key_id, id0); assert_eq!( pre.header.session_key_id, protocol::session_key_id(&key_epoch0) ); assert_eq!( post.header.session_key_id, protocol::session_key_id(&key_epoch1) ); assert_eq!( protocol::decrypt_body(&pre, &key_epoch0, protocol::SERVER_TO_CLIENT).unwrap(), b"before" ); assert_eq!( protocol::decrypt_body(&post, &key_epoch1, protocol::SERVER_TO_CLIENT).unwrap(), b"after" ); // A stale-epoch packet under the wrong key is rejected via session_key_id // BEFORE any AEAD work — ignorable, not a fatal decrypt error. let err = protocol::decrypt_body(&post, &key_epoch0, protocol::SERVER_TO_CLIENT).unwrap_err(); assert!(err.to_string().contains("session key id")); } #[test] fn check_native_protocol_version_names_the_mismatch() { use dosh::native::{NATIVE_PROTOCOL_VERSION, check_native_protocol_version}; check_native_protocol_version(NATIVE_PROTOCOL_VERSION, "server").unwrap(); let err = check_native_protocol_version(NATIVE_PROTOCOL_VERSION.wrapping_add(1), "server") .unwrap_err(); let message = err.to_string(); assert!( message.contains(protocol::VERSION_MISMATCH_REASON), "expected actionable upgrade message, got {message:?}" ); assert!( message.contains("server"), "should name the wrong peer: {message:?}" ); } #[test] fn replay_window_rejects_duplicates_but_allows_bounded_out_of_order() { let mut replay = ReplayWindow::new(8); assert!(replay.accept(10)); assert!(!replay.accept(10)); assert!(replay.accept(12)); assert!(replay.accept(11)); assert!(!replay.accept(11)); assert!(replay.accept(18)); assert!(!replay.accept(9)); assert!(!replay.accept(0)); }