Test Nostr encryption compatibility and forged relay messages
Demo images / Build & push demo images (push) Failing after 52s
Demo images / Build & push demo images (push) Failing after 52s
This commit is contained in:
@@ -70,6 +70,8 @@ mod node_message;
|
|||||||
mod nostr_discovery;
|
mod nostr_discovery;
|
||||||
mod nostr_handshake;
|
mod nostr_handshake;
|
||||||
mod nostr_relays;
|
mod nostr_relays;
|
||||||
|
#[cfg(test)]
|
||||||
|
mod nostr_security_tests;
|
||||||
mod peers;
|
mod peers;
|
||||||
mod port_allocator;
|
mod port_allocator;
|
||||||
mod rate_limit;
|
mod rate_limit;
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
//! Compatibility and hostile-relay regression tests for the 0.44 security update.
|
||||||
|
//! Loopback sockets run only through scripts/test-backend-isolated.sh.
|
||||||
|
use nostr_sdk::prelude::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn native_signer_encryption_remains_compatible_and_rejects_hostile_payloads() {
|
||||||
|
let alice = Keys::generate();
|
||||||
|
let bob = Keys::generate();
|
||||||
|
let message = "Archipelago signing compatibility — \u{1f30a}";
|
||||||
|
let encrypted = nip44::encrypt(
|
||||||
|
alice.secret_key(),
|
||||||
|
&bob.public_key(),
|
||||||
|
message,
|
||||||
|
nip44::Version::V2,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
nip44::decrypt(bob.secret_key(), &alice.public_key(), &encrypted).unwrap(),
|
||||||
|
message
|
||||||
|
);
|
||||||
|
let encrypted = nip04::encrypt(alice.secret_key(), &bob.public_key(), message).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
nip04::decrypt(bob.secret_key(), &alice.public_key(), encrypted).unwrap(),
|
||||||
|
message
|
||||||
|
);
|
||||||
|
// Valid v2 prefix followed by a payload exceeding the codec's maximum.
|
||||||
|
// This must fail at the size gate, before allocation/decoding/HMAC work.
|
||||||
|
let oversized = format!("AgAA{}", "A".repeat(100_000));
|
||||||
|
assert!(matches!(
|
||||||
|
nip44::decrypt(bob.secret_key(), &alice.public_key(), oversized),
|
||||||
|
Err(nip44::Error::MessageTooLong)
|
||||||
|
));
|
||||||
|
for malformed in ["", "Ag==", "not base64!", "?iv=", "YQ==?iv=YQ=="] {
|
||||||
|
assert!(nip04::decrypt(bob.secret_key(), &alice.public_key(), malformed).is_err());
|
||||||
|
assert!(nip44::decrypt(bob.secret_key(), &alice.public_key(), malformed).is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn relay_cannot_substitute_forged_fields_for_a_known_event_id() {
|
||||||
|
use futures_util::{SinkExt, StreamExt};
|
||||||
|
use tokio::net::TcpListener;
|
||||||
|
use tokio_tungstenite::{accept_async, tungstenite::Message};
|
||||||
|
let test = async {
|
||||||
|
let author = Keys::generate();
|
||||||
|
let known = EventBuilder::text_note("already verified")
|
||||||
|
.sign_with_keys(&author)
|
||||||
|
.unwrap();
|
||||||
|
let valid = EventBuilder::text_note("new legitimate message")
|
||||||
|
.sign_with_keys(&author)
|
||||||
|
.unwrap();
|
||||||
|
let mut forged_content = known.clone();
|
||||||
|
forged_content.content = "forged content".into();
|
||||||
|
let mut forged_author = known.clone();
|
||||||
|
forged_author.pubkey = Keys::generate().public_key();
|
||||||
|
let mut forged_signature = known.clone();
|
||||||
|
forged_signature.sig = valid.sig;
|
||||||
|
for forged in [&forged_content, &forged_author, &forged_signature] {
|
||||||
|
assert!(forged.verify().is_err());
|
||||||
|
}
|
||||||
|
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||||
|
let url = format!("ws://{}", listener.local_addr().unwrap());
|
||||||
|
let expected_id = valid.id;
|
||||||
|
let relay = tokio::spawn(async move {
|
||||||
|
let (socket, _) = listener.accept().await.unwrap();
|
||||||
|
let mut socket = accept_async(socket).await.unwrap();
|
||||||
|
while let Some(Ok(Message::Text(text))) = socket.next().await {
|
||||||
|
let message: serde_json::Value = serde_json::from_str(&text).unwrap();
|
||||||
|
if message[0] != "REQ" {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let subscription = message[1].as_str().unwrap();
|
||||||
|
for event in [&forged_content, &forged_author, &forged_signature, &valid] {
|
||||||
|
socket
|
||||||
|
.send(Message::Text(
|
||||||
|
serde_json::json!(["EVENT", subscription, event]).to_string(),
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
socket
|
||||||
|
.send(Message::Text(
|
||||||
|
serde_json::json!(["EOSE", subscription]).to_string(),
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let client = Client::new(Keys::generate());
|
||||||
|
client.database().save_event(&known).await.unwrap();
|
||||||
|
client.add_relay(&url).await.unwrap();
|
||||||
|
client
|
||||||
|
.try_connect_relay(&url, std::time::Duration::from_secs(3))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let events = client
|
||||||
|
.fetch_events(
|
||||||
|
Filter::new().kind(Kind::TextNote),
|
||||||
|
std::time::Duration::from_secs(5),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
events.len(),
|
||||||
|
1,
|
||||||
|
"forged events must never reach SDK consumers"
|
||||||
|
);
|
||||||
|
assert_eq!(events.iter().next().unwrap().id, expected_id);
|
||||||
|
client.disconnect().await;
|
||||||
|
relay.abort();
|
||||||
|
};
|
||||||
|
tokio::time::timeout(std::time::Duration::from_secs(15), test)
|
||||||
|
.await
|
||||||
|
.expect("local relay test timed out");
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user