bitchat/bitchatTests/NostrInboundPipelineTimestampTests.swift
jack 6f961638f2
Timestamp sanity windows: future-dated QR codes and implausible Nostr DM rumors (#1647)
Two re-assessment findings:

- verifyScannedQR checked only staleness (now - ts > maxAge), so a
  future-dated timestamp bought a QR a longer validity window than a
  fresh one. The freshness check is now symmetric.

- Inbound Nostr DMs had no client-side created_at validation; the age
  bound relied entirely on relays honoring the subscription's `since`
  filter. Both gift-wrap decrypt paths now drop rumors outside
  [now - lookback - skew, now + skew]. The inner rumor timestamp is the
  sender's true send time (only the outer gift wrap is randomized per
  NIP-17), so the window mirrors exactly what an honest relay already
  guarantees — a dishonest relay can no longer inject stale or
  future-dated DMs. Existing mitigations (persistent gift-wrap dedup,
  relay-side since) are unchanged; this closes the malicious-relay gap.

Co-authored-by: jack <jackjackbits@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 11:39:00 +02:00

33 lines
1.3 KiB
Swift

//
// NostrInboundPipelineTimestampTests.swift
// bitchatTests
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import Foundation
import Testing
@testable import bitchat
struct NostrInboundPipelineTimestampTests {
private let now = Date(timeIntervalSince1970: 1_700_000_000)
private let nowSeconds = 1_700_000_000
private let skew = Int(TransportConfig.nostrDMMaxClockSkewSeconds)
private let lookback = Int(TransportConfig.nostrDMSubscribeLookbackSeconds)
@Test("Rumor timestamps inside the lookback-plus-skew window are accepted")
func acceptsPlausibleTimestamps() {
#expect(NostrInboundPipeline.isPlausibleRumorTimestamp(nowSeconds, now: now))
#expect(NostrInboundPipeline.isPlausibleRumorTimestamp(nowSeconds - lookback + 60, now: now))
// A sender clock slightly ahead of the receiver is tolerated.
#expect(NostrInboundPipeline.isPlausibleRumorTimestamp(nowSeconds + skew - 60, now: now))
}
@Test("Future-dated and stale rumor timestamps are rejected")
func rejectsImplausibleTimestamps() {
#expect(!NostrInboundPipeline.isPlausibleRumorTimestamp(nowSeconds + skew + 60, now: now))
#expect(!NostrInboundPipeline.isPlausibleRumorTimestamp(nowSeconds - lookback - skew - 60, now: now))
}
}