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 WebhookVerifierKind — Hmac | 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 WebhookVerifierKind — HmacTimestamped (Stripe-style) — that:
- Parses the signature header into comma-separated
key=value pairs; reads the timestamp field (default t) and one or more signature fields (default v1).
- Computes
HMAC-SHA256(secret, "{t}{separator}{rawBody}") (default separator .) and constant-time-compares against each provided signature (a match on any wins — supports rotation).
- 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.
- 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/.
Summary
NetClaw's inbound-webhook
Hmacverifier 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 headert={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.Verifyswitches onWebhookVerifierKind—Hmac|HeaderSecret; unknown kinds throw (src/Netclaw.Daemon/Webhooks/WebhookRequestVerifier.cs:20-25).Hmacmode computesprefix + hex(HMAC-SHA256(secret, rawBodyBytes))and constant-time-compares it against the value ofSignatureHeaderName(WebhookRequestVerifier.cs:28-50;ComputeExpectedSha256:75-80). The body alone is signed — there is no timestamp concept and no structured-value parsing.WebhookVerificationConfig(Kind, Secret, SignatureHeaderName, SignaturePrefix, EventHeaderName, DeliveryIdHeaderName, HmacAlgorithm), exposed viaset_webhook/netclaw webhooks set.The gap
A provider sending
Header: t=1719000000,v1=abcd…where the signature isHMAC-SHA256(secret, "1719000000.{body}")is always rejectedinvalid_signature, because NetClaw (a) compares the wholet=…,v1=…string against a bare hex digest, and (b) hashes the body without the{timestamp}.prefix.HeaderSecretmode doesn't help — these providers don't send a static secret header.Reference format (Stripe / TextForge — identical shape)
t={unixSeconds},v1={lowerHex}(comma-separated fields; can carry multiplev1signatures for key rotation)."{t}.{rawBody}", HMAC-SHA256, lowercase hex.|now - t|exceeds a tolerance (Stripe and TextForge both use ~5 minutes).Stripe-Signature). TextForge: https://textforge.net/docs/guides/webhooks (X-TextForge-Signature).Proposal
Add a third
WebhookVerifierKind—HmacTimestamped(Stripe-style) — that:key=valuepairs; reads the timestamp field (defaultt) and one or more signature fields (defaultv1).HMAC-SHA256(secret, "{t}{separator}{rawBody}")(default separator.) and constant-time-compares against each provided signature (a match on any wins — supports rotation).tvs. server time (default 300s); rejectstimestamp_out_of_toleranceoutside it — the replay protection the timestamp exists for.HmacAlgorithmslot (Sha256today).New optional
WebhookVerificationConfigfields, all with defaults so the common Stripe/TextForge case is zero-config:ToleranceSeconds(300),TimestampField(t),SignatureField(v1),SignedPayloadSeparator(.). Surface them onset_webhook/ CLI /WebhookRouteValidator.Backwards-compatibility requirements
Purely additive and opt-in — an upgrade is a no-op for anyone not using it:
Hmac/HeaderSecretbehavior byte-for-byte unchanged — same bytes hashed, same compare;ComputeExpectedSha256untouched."Hmac"/"HeaderSecret"route files keep verifying identically; the new kind is chosen explicitly.~/.netclaw/config/webhooks/*.jsondeserialize and behave exactly as before. No migration.set_webhook/CLI new params optional — omitting them yields routes identical to today.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/.