Widen AP signature freshness window from 30s to a few minutes #102

Closed
opened 2026-07-15 19:51:33 +00:00 by claude-bot · 1 comment
Contributor

Severity: Low · Confidence: Medium · Effort: XS · Category: code

Evidence

  • api/src/lib/apCrypto.ts:198,244 - Math.abs(Date.now() - reqTime) > 30_000.

Problem
The 30-second date freshness window is far tighter than the fediverse norm (Mastodon et al. allow ~1-12 h). Legitimate peers with >30s clock drift (no NTP guarantee) are silently rejected as invalid signatures. Signature verification is otherwise well-built (covers (request-target), date, digest; verifies digest against rawBody; binds keyId?actor on the entry path).

Impact
Intermittent, hard-to-diagnose federation failures with well-behaved but slightly-skewed peers.

Fix
Widen to ~300s, keeping replay protection meaningful.

Acceptance criteria

  • A signed request with ~1 min clock skew verifies; a 1-hour-old one is still rejected.

Filed from the 2026-07-15 codebase audit. Full report: docs/.internal/report-2026-07-15.md (gitignored).

**Severity:** Low · **Confidence:** Medium · **Effort:** XS · Category: code **Evidence** - `api/src/lib/apCrypto.ts:198,244` - `Math.abs(Date.now() - reqTime) > 30_000`. **Problem** The 30-second `date` freshness window is far tighter than the fediverse norm (Mastodon et al. allow ~1-12 h). Legitimate peers with >30s clock drift (no NTP guarantee) are silently rejected as invalid signatures. Signature verification is otherwise well-built (covers `(request-target)`, `date`, `digest`; verifies digest against rawBody; binds keyId?actor on the entry path). **Impact** Intermittent, hard-to-diagnose federation failures with well-behaved but slightly-skewed peers. **Fix** Widen to ~300s, keeping replay protection meaningful. **Acceptance criteria** - [ ] A signed request with ~1 min clock skew verifies; a 1-hour-old one is still rejected. --- _Filed from the 2026-07-15 codebase audit. Full report: `docs/.internal/report-2026-07-15.md` (gitignored)._
Author
Contributor

Fixed in c1ed843 (v7.2.0 wave 2).

Extracted SIGNATURE_FRESHNESS_MS = 300_000 in apCrypto.ts and applied it to both verifiers.

Worth noting: the issue cites lines 198 and 244, and the second one nearly got missed — verifyGetSignature returns null rather than false, so a naive replace of the verifyInboxSignature line leaves the GET path (signed reads of a host project, #62) still on 30s. Both are now on the shared constant; grepping for 30_000 in that file returns nothing.

Acceptance criteria:

  • A signed request with ~1 min clock skew verifies — tested in both directions (±60s), since a peer can be ahead of our clock as easily as behind.
  • A 1-hour-old one is still rejected.

I also had to retarget the existing replay test: it used a date exactly 5 * 60_000 old, which against a 300_000 ms window would have been decided by however many milliseconds elapsed between constructing the date and evaluating the comparison — a coin-flip. It now uses 1 hour, matching this issue's acceptance criteria.

Replay protection remains meaningful: the digest is still verified against the raw body unconditionally, and (request-target)/date/digest must all be signed, so a captured signature is only reusable verbatim within 5 minutes.

CI green (242/242).

Fixed in `c1ed843` (v7.2.0 wave 2). Extracted `SIGNATURE_FRESHNESS_MS = 300_000` in `apCrypto.ts` and applied it to **both** verifiers. **Worth noting:** the issue cites lines 198 and 244, and the second one nearly got missed — `verifyGetSignature` returns `null` rather than `false`, so a naive replace of the `verifyInboxSignature` line leaves the GET path (signed reads of a host project, #62) still on 30s. Both are now on the shared constant; grepping for `30_000` in that file returns nothing. **Acceptance criteria:** - [x] A signed request with ~1 min clock skew verifies — tested in **both directions** (`±60s`), since a peer can be ahead of our clock as easily as behind. - [x] A 1-hour-old one is still rejected. I also had to retarget the existing replay test: it used a date exactly `5 * 60_000` old, which against a 300_000 ms window would have been decided by however many milliseconds elapsed between constructing the date and evaluating the comparison — a coin-flip. It now uses 1 hour, matching this issue's acceptance criteria. Replay protection remains meaningful: the digest is still verified against the raw body unconditionally, and `(request-target)`/`date`/`digest` must all be signed, so a captured signature is only reusable verbatim within 5 minutes. CI green (242/242).
Sign in to join this conversation.
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
rbrooks/TeaLeaves#102
No description provided.