Skip to content

Add a Stripe-style timestamped-HMAC webhook signature verifier (t=,v1= over {timestamp}.{body}) #1658

Description

@Aaronontheweb

Summary

NetClaw's inbound-webhook Hmac verifier only supports the "GitHub-style" scheme — HMAC-SHA256 over the raw request body, emitted as a plain (optionally prefixed) hex string in a single header. Many major webhook providers instead use a timestamped scheme: a structured signature header t={unix},v1={hex} whose signature is computed over "{timestamp}.{body}", with the recipient enforcing a timestamp tolerance window for replay protection. Stripe (Stripe-Signature) and TextForge (X-TextForge-Signature) both use this exact format. NetClaw currently cannot verify these — add a verifier kind that does.

Current behavior (verified in source)

  • WebhookRequestVerifier.Verify switches on WebhookVerifierKindHmac | HeaderSecret; unknown kinds throw (src/Netclaw.Daemon/Webhooks/WebhookRequestVerifier.cs:20-25).
  • Hmac mode computes prefix + hex(HMAC-SHA256(secret, rawBodyBytes)) and constant-time-compares it against the value of SignatureHeaderName (WebhookRequestVerifier.cs:28-50; ComputeExpectedSha256 :75-80). The body alone is signed — there is no timestamp concept and no structured-value parsing.
  • Config is WebhookVerificationConfig (Kind, Secret, SignatureHeaderName, SignaturePrefix, EventHeaderName, DeliveryIdHeaderName, HmacAlgorithm), exposed via set_webhook / netclaw webhooks set.

The gap

A provider sending Header: t=1719000000,v1=abcd… where the signature is HMAC-SHA256(secret, "1719000000.{body}") is always rejected invalid_signature, because NetClaw (a) compares the whole t=…,v1=… string against a bare hex digest, and (b) hashes the body without the {timestamp}. prefix. HeaderSecret mode doesn't help — these providers don't send a static secret header.

Reference format (Stripe / TextForge — identical shape)

Proposal

Add a third WebhookVerifierKindHmacTimestamped (Stripe-style) — that:

  1. Parses the signature header into comma-separated key=value pairs; reads the timestamp field (default t) and one or more signature fields (default v1).
  2. Computes HMAC-SHA256(secret, "{t}{separator}{rawBody}") (default separator .) and constant-time-compares against each provided signature (a match on any wins — supports rotation).
  3. Enforces a tolerance window on t vs. server time (default 300s); rejects timestamp_out_of_tolerance outside it — the replay protection the timestamp exists for.
  4. Reuses the existing HmacAlgorithm slot (Sha256 today).

New optional WebhookVerificationConfig fields, all with defaults so the common Stripe/TextForge case is zero-config: ToleranceSeconds (300), TimestampField (t), SignatureField (v1), SignedPayloadSeparator (.). Surface them on set_webhook / CLI / WebhookRouteValidator.

Backwards-compatibility requirements

Purely additive and opt-in — an upgrade is a no-op for anyone not using it:

  • Existing Hmac/HeaderSecret behavior byte-for-byte unchanged — same bytes hashed, same compare; ComputeExpectedSha256 untouched.
  • Additive enum value only — existing serialized "Hmac"/"HeaderSecret" route files keep verifying identically; the new kind is chosen explicitly.
  • New config fields optional with defaults — existing ~/.netclaw/config/webhooks/*.json deserialize and behave exactly as before. No migration.
  • set_webhook/CLI new params optional — omitting them yields routes identical to today.
  • No weakening — keep the constant-time compare; the new mode is stricter (adds the mandatory timestamp/replay check). Malformed or missing header fields → clean reject, never a crash.
  • Endpoint/dispatch untouched — routing, event allowlist, dedup, rate-limit, and payload→session formatting all unchanged; only a verification branch is added.
  • Downgrade — an older daemon reading a route file that uses the new kind should fail that route cleanly (it already throws on unknown kinds), never silently accept-all.

Out of scope

Provider-specific key derivation (e.g. a sender that HMACs with Base64(SHA256(secret)) rather than the raw secret) is a sender-side concern; NetClaw uses the configured secret as the HMAC key per the standard.

Anchors

src/Netclaw.Daemon/Webhooks/WebhookRequestVerifier.cs:20-25,28-50,75-80 · src/Netclaw.Actors/Tools/SetWebhookTool.cs (config surface) · WebhookVerifierKind / WebhookVerificationConfig (Netclaw.Configuration) · route store ~/.netclaw/config/webhooks/.

Metadata

Metadata

Assignees

No one assigned

    Labels

    configConfiguration issues, netclaw doctor, schema validation.securitySecurity-related changeswebhooksIssues related to inbound netclaw webhooks

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions