security fix(gateway): defend pre-auth device-signature verify against CPU DoS#77492
security fix(gateway): defend pre-auth device-signature verify against CPU DoS#77492fede-kamel wants to merge 5 commits into
Conversation
|
Codex review: needs maintainer review before merge. Reviewed July 6, 2026, 4:58 PM ET / 20:58 UTC. Summary PR surface: Source +146, Tests +455, Other +349. Total +950 across 11 files. Reproducibility: yes. Source inspection on current main shows device-bearing connects can reach device-id derivation and v3/v2 signature verification before any device-signature limiter; the PR also supplies terminal live output and focused WS integration tests, though I did not rerun the flood PoC. Review metrics: 3 noteworthy metrics.
Root-cause cluster Members:
Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything. Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Risk before merge
Maintainer options:
Next step before merge
Maintainer decision needed
Security Review detailsBest possible solution: Land this layered mitigation after gateway/security owners accept the field bounds, per-IP lockout behavior, and PoC placement; otherwise tune those policy choices without losing the pre-crypto short-circuit. Do we have a high-confidence way to reproduce the issue? Yes. Source inspection on current main shows device-bearing connects can reach device-id derivation and v3/v2 signature verification before any device-signature limiter; the PR also supplies terminal live output and focused WS integration tests, though I did not rerun the flood PoC. Is this the best way to solve the issue? Yes, mechanically this is the right layer: the gate runs before createPublicKey/verify, malformed inputs are bounded before crypto, and lockouts use the canonical auth rate-limit contract. The remaining choice is owner acceptance of the protocol bounds and availability tradeoff. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 6bffda416614. Label changesLabel justifications:
Evidence reviewedPR surface: Source +146, Tests +455, Other +349. Total +950 across 11 files. View PR surface stats
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
Review history (5 earlier review cycles)
|
3bf503e to
4a844cf
Compare
|
Addressing the two Codex findings on 1. Limiter now runs before any pre-auth crypto ( Hoisted the 2. Bucket reset deferred until after Two changes:
Net behavior: an attacker with their own keypair producing a valid self-signature still consumes the bucket because Coverage added in
Test results: Two pre-existing assertions in Re-review progress:
|
4a844cf to
49334e7
Compare
|
@steipete when you have a moment — security fix for pre-auth device-signature CPU DoS, CI green. Will rebase to clear the conflicts before merge. Thanks! |
4b6b34a to
3166348
Compare
|
cc @openclaw/openclaw-secops for visibility — rebased onto main, mergeable. Just routing through the right team. No rush. Thanks! |
3166348 to
6233fc0
Compare
|
Rebased onto current @openclaw/openclaw-secops — flagging for review (pre-auth device-signature CPU-DoS hardening). |
1665085 to
e9a9402
Compare
…e-auth signature gate The pre-auth device-signature rate-limit gate records a failure for every device-carrying handshake before crypto. Forged bootstrap-token attempts carry a valid self-signed device identity, so they tripped the signature gate and were rejected as `device-signature-rate-limited`, shadowing the existing bootstrap-token rate-limit contract whose reason is `rate_limited`. When a bootstrap- or device-token candidate is present, surface that credential's `rate_limited` reason at the gate instead of the signature scope reason. The crypto bound is unchanged: lockout still returns before any createPublicKey/verify work, so the device-signature scope still guards pure signature-only handshakes.
e9a9402 to
6640657
Compare
|
Review finding addressed in f20a530 — pure device-signature lockouts now route through the canonical auth What changed: the signature-gate lockout branch in Bonus simplification: the June 14 token-candidate special case ( Note on Tests updated to assert the canonical contract: Verification: |
…ate_limited contract Pure device-signature lockouts previously returned an ad-hoc device-auth-invalid response with a signature-scope reason, hiding rate-limit state from clients, telemetry severity, and retry guidance. All signature-gate lockouts now reject through rejectUnauthorized with reason rate_limited, surfacing AUTH_RATE_LIMITED, authReason rate_limited, wait_then_retry guidance, and the high-severity security event — the same contract as bootstrap/device-token lockouts. This also deletes the token-candidate special case since both branches now share one path. The pre-crypto short-circuit is unchanged: lockout still returns before any createPublicKey/verify work. Focused tests assert AUTH_RATE_LIMITED/authReason=rate_limited on the lockout responses across forged-signature, PEM, shared-bucket, and valid-self-signature scenarios.
|
@clawsweeper re-review Head @openclaw/openclaw-secops — this implements the review's recommended option 1 (canonical lockout contract, pre-crypto short-circuit preserved). The remaining acceptance items the review routes to security owners: the device.publicKey/signature protocol bounds (sized for every valid Ed25519 form with margin) and the per-IP pre-auth limiter's availability tradeoff for browser-origin clients behind shared IPs. |
|
🦞🧹 I asked ClawSweeper to review this item again. |
|
This pull request has been automatically marked as stale due to inactivity. |
Closes #77979
Summary
deviceblock rancrypto.createPublicKeyplus a v3-then-v2crypto.verifyper request, gated only by frame-size limits. An unauthenticated remote attacker could pin a single Gateway core: PoC against a real running gateway hits ~510 forged handshakes/s/IP, degrading legit handshake p50 by 48.7× (1.16 ms → 56.6 ms) and p99 by 94.7×.device.publicKey(1024) anddevice.signature(256); shape pre-check onverifyDeviceSignature/deriveDeviceIdFromPublicKey/normalizeDevicePublicKeyBase64Urlrejecting inputs that cannot decode to a 32-byte raw key or 64-byte signature; newAUTH_RATE_LIMIT_SCOPE_DEVICE_SIGNATUREbucket gating the verify per IP. The browser-origin rate limiter is always constructed (server.impl.ts:424), so the gate fires for browser-origin clients even when nogateway.auth.rateLimitis configured. For non-browser-origin clients, the gate fires when the operator configures a rate limiter (the typical production case).Reproduction
The PoC (
scripts/poc-preauth-flood-ws.mjs) opens N concurrent WS connections, each completing the connect-challenge handshake with a real Ed25519 PEM public key and a random 64-byte signature. The gateway reaches the verify path (server-issued nonce echoed back,device.idmatchessha256(rawPublicKey)) and runscreatePublicKey+ v3verify+ v2verifyper request. A separate measurement client probes time-to-first-event in parallel.Against a vulnerable gateway:
Observed:
To validate the fix end-to-end without exposing a dev gateway to the LAN, the in-process integration test runs the same forged-handshake sequence against a real
withGatewayServerinstance configured withrateLimit.maxAttempts: 1, exemptLoopback: true:The test asserts:
error.details.reason === "device-signature"(the verify ran).error.details.reason === "device-signature-rate-limited"andretryAfterMs > 0— the gate short-circuited before any crypto.Reverting the
recordFailureline in the rate-limit gate makes both tests fail withexpected 'device-signature' to be 'device-signature-rate-limited'.Test plan
src/gateway/server.preauth-signature-rate-limit.test.ts— new in-process WS integration test (2 cases). Both fail when the fix is disabled.src/infra/device-identity.test.ts— new shape pre-check tests: oversized PEM, oversized non-PEM, signature-shaped input rejected as public key, public-key-shaped input rejected as signature, all without invoking crypto.src/gateway/protocol/connect-device-bounds.test.ts— new schema-cap tests:validateConnectParamsrejects oversizeddevice.publicKeyanddevice.signature; accepts them at the documented bounds.src/gateway/server/ws-connection/handshake-auth-helpers.test.ts— 27/27 pass, no regression.Verification
pnpm testof the four affected files: 41/41 pass.grep AUTH_RATE_LIMIT_SCOPE_DEVICE_SIGNATUREon the bundle).pnpm tsgoare pre-existing and unrelated (pi-embedded-runner.*AgentMessage casts and missingweb-tree-sitterdep).Real behavior proof
deviceblocks rancrypto.createPublicKeyplus a v3-then-v2crypto.verifyper request with no per-IP cap. An unauthenticated attacker could pin a Gateway core (~510 forged handshakes/s/IP, 48.7× p50 / 94.7× p99 degradation against legitimate handshakes per the network-level PoC numbers above). The fix layers three defenses: ajv schema caps reject oversizeddevice.publicKeyanddevice.signaturebefore any handler runs;isPlausibleDevicePublicKeyInput/isPlausibleDeviceSignatureInputshape pre-checks reject inputs that cannot decode to a 32-byte raw key or 64-byte signature without invoking crypto; andAUTH_RATE_LIMIT_SCOPE_DEVICE_SIGNATUREgates the verify per IP, so once the bucket is exhausted nocreatePublicKeyorverifyruns.security/preauth-signature-rate-limitHEAD6233fc0e60). No vitest, no mocks. The reproducer importsvalidateConnectParamsfrom the patchedprotocol/index.ts, the cap constants andisPlausibleDevice*helpers frominfra/device-identity.ts, andcreateAuthRateLimiterfromgateway/auth-rate-limit.ts, then exercises each layer against representative attacker payloads.device.publicKey/device.signature(including a 60 KB representative attacker payload), shape pre-checks against malformed inputs that decode to wrong-length raw bytes, and theAUTH_RATE_LIMIT_SCOPE_DEVICE_SIGNATUREbucket fired against a single attacker IP. Run asnode --import tsx <local-copy>.mtsfrom a worktree on this branch.PASSand every cap+1 / 60 KB / wrong-length / wrong-shape attacker input validatedREJECTacross both schema (Layer 1) and shape pre-check (Layer 2). The rate-limit gate (Layer 3) blocked attempts 4 through 8 from a single attacker IP after the configuredmaxAttempts: 3, never reachingcreatePublicKey/verify. Layer 3b measured the per-op cost of the full Ed25519 verify path (0.043 ms/op) versus the gate-blocked path (0.013 ms/op) — a 3.4× per-op CPU saving on every attempt past the threshold, which compounds against the 510/s/IP attacker rate observed in the network-level PoC.server.preauth-signature-rate-limit.test.ts(in-process WS server) and by the livescripts/poc-preauth-flood-ws.mjsPoC against a runningpnpm gateway:watch. Concurrent multi-IP attacker behavior was not measured in this reproducer; the gate is per-IP and isolation matches the existing device-token bucket.Notes for reviewer
triage: dirty-candidatebecause the diff touches schema, device-identity helpers, and the WS message handler. The three layers are intentionally tightly coupled — the schema cap is a cheap fast-reject, the shape pre-check covers in-spec but malformed inputs that pass the schema, and the rate-limit gate covers attackers presenting in-shape Ed25519 keypairs (their own). Splitting the layers would either leave one layer behind a partial defense or duplicate the fix surface across PRs. Happy to split if reviewers prefer one layer per PR.recordFailureis intentionally pessimistic before crypto runs — an attacker with their own keypair producing valid self-signatures should still consume the bucket.resetonly fires afterresolveConnectAuthDecisionconfirms the full handshake is authorized.