mirror of
https://relay.ngit.dev/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit-grasp.git
synced 2026-10-05 15:08:24 +00:00
231 lines
8.8 KiB
Markdown
231 lines
8.8 KiB
Markdown
# Event Signature Verification
|
|
|
|
**ID:** 1bb0
|
|
|
|
## Issue
|
|
|
|
We need to verify that event signatures are being checked for every event we store or issue over websockets. Currently unclear if rust-nostr is doing this automatically or if we need explicit verification.
|
|
|
|
## Requirements
|
|
|
|
1. **Verify signatures for all events** - Both stored events and events sent over websockets
|
|
2. **Check signatures before expensive operations** - Validate signatures before database queries for efficiency
|
|
3. **Avoid duplicate verification** - Don't verify the same event multiple times
|
|
4. **NaughtyList integration** - If an event with invalid signature comes via sync, add the source Relay to the NaughtyList
|
|
|
|
## Investigation Questions
|
|
|
|
- [x] Does rust-nostr automatically verify signatures when parsing events?
|
|
- [x] Where do events enter the system? (websocket, sync, etc.)
|
|
- [x] Where should verification happen in the event flow?
|
|
- [x] What is the current NaughtyList implementation status?
|
|
|
|
## Investigation Findings
|
|
|
|
### Event Entry Points
|
|
|
|
Events enter ngit-grasp through two paths:
|
|
|
|
1. **WebSocket (User-submitted)** - Handled by `nostr-relay-builder`'s `LocalRelay`
|
|
2. **Sync (From other relays)** - Handled by `nostr-sdk`'s `RelayPool` via `RelayConnection`
|
|
|
|
### Current Signature Verification Status
|
|
|
|
#### Path 1: WebSocket Events (LocalRelay) - VERIFIED
|
|
|
|
`nostr-relay-builder/src/local/inner.rs` line ~340-380 in `handle_client_msg`:
|
|
|
|
```rust
|
|
if !event.verify_id() {
|
|
return send_msg(..., "invalid event ID", ...);
|
|
}
|
|
|
|
// ... check expired, POW ...
|
|
|
|
if !event.verify_signature() {
|
|
return send_msg(..., "invalid event signature", ...);
|
|
}
|
|
|
|
// THEN check write policy (our Nip34WritePolicy)
|
|
if let Some(policy) = self.write_policy.as_ref() {
|
|
if let WritePolicyResult::Reject { ... } = policy.admit_event(&event, addr).await {
|
|
// ...
|
|
}
|
|
}
|
|
```
|
|
|
|
**Validation order for WebSocket events:**
|
|
1. Rate limiting
|
|
2. ID verification (`verify_id()`)
|
|
3. Expiration check
|
|
4. POW check (if configured)
|
|
5. **Signature verification (`verify_signature()`)** - BEFORE any DB queries
|
|
6. NIP42 auth check
|
|
7. Protected event check
|
|
8. Database duplicate check
|
|
9. Mode check
|
|
10. **WritePolicy** (our `Nip34WritePolicy.admit_event()`)
|
|
|
|
**Conclusion:** WebSocket events ARE verified before reaching our code. Order is correct.
|
|
|
|
#### Path 2: Sync Events (RelayPool) - VERIFIED
|
|
|
|
`nostr-relay-pool/src/relay/inner.rs` in `handle_event_msg`:
|
|
|
|
```rust
|
|
// Check if the event was already verified.
|
|
// This is useful if someone continues to send the same invalid event:
|
|
// since invalid events aren't stored in the database,
|
|
// skipping this check would result in the re-verification of the event.
|
|
// This is important since event signature verification is a heavy job!
|
|
if !self.state.verified(&event.id)? {
|
|
event.verify()?; // This calls verify_id() AND verify_signature()
|
|
}
|
|
|
|
// Save into the database
|
|
match self.state.database().save_event(&event).await? { ... }
|
|
```
|
|
|
|
**Important:** nostr-sdk has a "verified events" cache to avoid re-verifying the same event.
|
|
|
|
**Validation order for Sync events:**
|
|
1. Event size check
|
|
2. Tags limit check
|
|
3. Subscription filter matching (if configured)
|
|
4. Expiration check
|
|
5. Admission policy check
|
|
6. Database status check (already saved/deleted?)
|
|
7. **Signature verification (event.verify())** - uses cache to avoid duplicates
|
|
8. Save to database
|
|
|
|
**Conclusion:** Sync events ARE verified by nostr-sdk before being stored.
|
|
|
|
### NaughtyList Status
|
|
|
|
The `NaughtyListTracker` exists at `src/sync/naughty_list.rs` (lines 27-557) and is used for:
|
|
- DNS lookup failures
|
|
- TLS certificate issues
|
|
- Protocol errors
|
|
|
|
**Categories defined:**
|
|
- `DnsLookupFailed` - Domain doesn't resolve
|
|
- `TlsCertificateInvalid` - SSL/TLS issues
|
|
- `ProtocolError` - WebSocket protocol violations
|
|
|
|
**NOT currently used for:** Invalid signatures from relays during sync.
|
|
|
|
### Gap Analysis
|
|
|
|
| Entry Point | Sig Verified? | Correct Order? | By Whom | NaughtyList on Invalid? |
|
|
|-------------|---------------|----------------|---------|-------------------------|
|
|
| WebSocket | YES | YES | nostr-relay-builder | N/A (direct client) |
|
|
| Sync | YES | YES | nostr-sdk relay pool | **NO - GAP** |
|
|
|
|
## Recommended Actions
|
|
|
|
### 1. No Code Changes Needed for Verification
|
|
|
|
Both entry points already verify signatures correctly:
|
|
- WebSocket: `nostr-relay-builder` verifies before calling our `WritePolicy`
|
|
- Sync: `nostr-sdk` verifies before storing and has dedup cache
|
|
|
|
### 2. Add NaughtyList for Invalid Signatures (Enhancement)
|
|
|
|
Currently, if a relay sends us an event with an invalid signature during sync, we simply reject the event but don't track the relay as "naughty". This could be valuable for:
|
|
- Detecting malicious/buggy relays
|
|
- Avoiding wasted bandwidth re-syncing with bad relays
|
|
- Metrics/monitoring
|
|
|
|
**Implementation approach:**
|
|
1. Add `InvalidEventSignature` to `NaughtyCategory` enum
|
|
2. Hook into sync error handling to detect signature verification failures
|
|
3. Record the source relay when signature verification fails
|
|
|
|
**Challenge:** nostr-sdk's relay pool handles verification internally. We'd need to either:
|
|
- Check for verification errors in the `RelayNotification` stream
|
|
- Or wrap events with pre-verification before they reach the pool (not recommended - duplicates work)
|
|
|
|
### 3. Optional: Add Explicit Verification in process_event_static (Defense in Depth)
|
|
|
|
Currently `src/sync/mod.rs:2389` `process_event_static()` trusts that events are pre-verified. While nostr-sdk does verify, we could add explicit verification as defense-in-depth:
|
|
|
|
```rust
|
|
// At start of process_event_static:
|
|
if event.verify().is_err() {
|
|
// Log and add to naughty list
|
|
return ProcessResult::Rejected;
|
|
}
|
|
```
|
|
|
|
**Trade-off:** This would re-verify events that nostr-sdk already verified (wasted CPU), unless nostr-sdk exposes its verified-events cache.
|
|
|
|
## Performance Issue: Verification Cache Not Shared Across Sync Connections
|
|
|
|
### Finding
|
|
|
|
nostr-sdk's verification cache is **per-RelayPool**, not global. The cache lives in `SharedState`:
|
|
|
|
```rust
|
|
// nostr-relay-pool/src/shared.rs
|
|
pub struct SharedState {
|
|
// ...
|
|
verification_cache: Arc<Mutex<LruCache<u64, ()>>>, // 128k entries max
|
|
// ...
|
|
}
|
|
```
|
|
|
|
When a `RelayPool` is created, it creates one `SharedState` and shares it (via Arc clone) with all relays in that pool. This means relays **within the same pool** share the verification cache.
|
|
|
|
### The Problem in ngit-grasp
|
|
|
|
In `src/sync/relay_connection.rs:112-114`:
|
|
|
|
```rust
|
|
pub fn new(url: String, keys: Keys) -> Self {
|
|
let client = Client::new(keys); // Creates a NEW Client for EACH relay connection!
|
|
// ...
|
|
}
|
|
```
|
|
|
|
**ngit-grasp creates a separate `Client` instance for each `RelayConnection`**. Each `Client` has its own `RelayPool`, which has its own `SharedState` with its own verification cache.
|
|
|
|
### Result
|
|
|
|
**If the same event is received from multiple relays during sync, it WILL be verified multiple times** because:
|
|
|
|
1. Each `RelayConnection` uses a separate `Client`
|
|
2. Each `Client` has its own isolated `RelayPool` and `SharedState`
|
|
3. The verification caches are NOT shared across `RelayConnection` instances
|
|
|
|
### Impact
|
|
|
|
- Wasted CPU cycles on redundant signature verification (expensive cryptographic operation)
|
|
- The more relays we sync from, the worse the duplication
|
|
- Popular events (seen on many relays) get verified N times instead of once
|
|
|
|
### Potential Solutions (Not Yet Researched)
|
|
|
|
**Open question:** How trivial is it to share the verification cache?
|
|
|
|
Options to investigate:
|
|
1. **Single shared `Client`/`RelayPool`** - Use one pool for all sync operations instead of per-relay clients
|
|
2. **Shared `SharedState`** - Would require upstream changes or workarounds to inject a shared state into multiple clients
|
|
3. **Application-level verification cache** - Implement our own cache at the ngit-grasp level, verify events before they reach the clients (but this duplicates the verification work on first-seen events)
|
|
4. **Upstream contribution** - Add ability to share verification cache across multiple `Client` instances
|
|
|
|
## Conclusion
|
|
|
|
**Good news:** Signature verification IS happening correctly for both entry points via rust-nostr libraries.
|
|
|
|
**Performance issue:** Verification cache is not shared across sync connections, causing redundant signature verification for events seen from multiple relays.
|
|
|
|
**Potential enhancement:** Add NaughtyList tracking for relays that send invalid signatures during sync. This requires monitoring for verification errors from nostr-sdk, which may require upstream changes or a custom wrapper.
|
|
|
|
## Status
|
|
|
|
**Created:** 2026-01-10
|
|
**Status:** Investigation Complete
|
|
**Finding:** No immediate action required - signatures are verified by upstream libraries
|
|
**Performance:** Verification cache isolation causes redundant verification across sync connections (needs further research on fix complexity)
|
|
**Enhancement:** Consider NaughtyList integration for invalid signatures from sync relays
|