Files
grim/tests/replay_check.rs
T
Claude ce1c071f3c Security hardening from adversarial audit
Wallet:
- identity.json (NIP-49 ncryptsec) now written 0600 in a 0700 dir so a local
  user can't grind the wallet password offline (+ regression test).
- Wallet password held as a ZeroingString through init_nostr so it's scrubbed
  on drop instead of lingering in a plain String for the session.
- Replaced 4 .unwrap() on re-read tx_meta with graceful guards (archive wipe
  mid-send could otherwise panic the nostr/task thread).
- Tor::http_request/post: bind the client once via let-else and propagate TLS
  builder errors, fixing a TOCTOU unwrap panic on concurrent Tor restart.

goblin-nip05d server (redeployed to goblin.st, verified live):
- One-name-per-pubkey now enforced by a partial UNIQUE index (closes the
  check-then-insert race); INSERT rows-affected==0 returns 409 not a false 201.
- NIP-98 replay protection: one-time auth event-id enforcement within the
  freshness window; tightened forward skew to +5s.
- Rate-limited the unauthenticated GET endpoints; SQLite in WAL mode.
- Verified live: replay rejected, second name for a pubkey blocked.

Audit verdict: fund-safety invariants (never auto-pay Invoice1; S2/I2
finalization bound to counterparty npub) and Tor-from-day-one all hold.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 02:13:15 -04:00

106 lines
2.9 KiB
Rust

use base64::Engine;
use nostr_sdk::prelude::*;
use sha2::{Digest, Sha256};
use std::process::Command;
#[tokio::test]
#[ignore]
async fn replay_and_double_name_rejected() {
let keys = Keys::generate();
let pk = keys.public_key().to_hex();
let server = "https://goblin.st";
// Build a register POST for name A.
let name_a = format!("r{}", &pk[..7]);
let url = format!("{server}/api/v1/register");
let body = serde_json::json!({"name": name_a, "pubkey": pk}).to_string();
let ph = hex::encode(Sha256::digest(body.as_bytes()));
let ev = EventBuilder::new(Kind::HttpAuth, "")
.tag(Tag::custom(TagKind::custom("u"), [url.clone()]))
.tag(Tag::custom(TagKind::custom("method"), ["POST".to_string()]))
.tag(Tag::custom(TagKind::custom("payload"), [ph]))
.sign_with_keys(&keys)
.unwrap();
let auth = format!(
"Nostr {}",
base64::engine::general_purpose::STANDARD.encode(ev.as_json())
);
let post = |a: &str, b: &str| {
String::from_utf8_lossy(
&Command::new("curl")
.args([
"-s",
"-X",
"POST",
&url,
"-H",
&format!("Authorization: {a}"),
"-H",
"Content-Type: application/json",
"-d",
b,
])
.output()
.unwrap()
.stdout,
)
.to_string()
};
let r1 = post(&auth, &body);
let r2 = post(&auth, &body); // exact replay (same auth event id)
println!("first: {r1}");
println!("replay: {r2}");
assert!(r1.contains("nip05"), "first register should succeed");
assert!(
r2.contains("replayed"),
"replay should be rejected, got: {r2}"
);
// Second DISTINCT name with a FRESH signature but same pubkey -> one-name rule.
let name_b = format!("s{}", &pk[..7]);
let body_b = serde_json::json!({"name": name_b, "pubkey": pk}).to_string();
let ph_b = hex::encode(Sha256::digest(body_b.as_bytes()));
let ev_b = EventBuilder::new(Kind::HttpAuth, "")
.tag(Tag::custom(TagKind::custom("u"), [url.clone()]))
.tag(Tag::custom(TagKind::custom("method"), ["POST".to_string()]))
.tag(Tag::custom(TagKind::custom("payload"), [ph_b]))
.sign_with_keys(&keys)
.unwrap();
let auth_b = format!(
"Nostr {}",
base64::engine::general_purpose::STANDARD.encode(ev_b.as_json())
);
let r3 = post(&auth_b, &body_b);
println!("2nd name: {r3}");
assert!(
r3.contains("already has a name") || r3.contains("pubkey already"),
"one-name rule should block, got: {r3}"
);
// Cleanup name A.
let del_url = format!("{server}/api/v1/register/{name_a}");
let ev_d = EventBuilder::new(Kind::HttpAuth, "")
.tag(Tag::custom(TagKind::custom("u"), [del_url.clone()]))
.tag(Tag::custom(
TagKind::custom("method"),
["DELETE".to_string()],
))
.sign_with_keys(&keys)
.unwrap();
let auth_d = format!(
"Nostr {}",
base64::engine::general_purpose::STANDARD.encode(ev_d.as_json())
);
let _ = Command::new("curl")
.args([
"-s",
"-X",
"DELETE",
&del_url,
"-H",
&format!("Authorization: {auth_d}"),
])
.output();
println!("✓ replay + one-name-per-pubkey enforced");
}