rust-web-server 17.113.0

Single-binary web server and reverse proxy: automatic HTTPS (Let's Encrypt), HTTP/1.1, HTTP/2 and HTTP/3, load balancing, auth and rate limiting, configured in one TOML file. Also a Rust library: routing, middleware, MCP server, ORM, background jobs. No hyper, no Tower; HTTP/2 and HTTP/3 framing via h2 and quinn.
Documentation
---
title: Webhook Signature Verification
description: Verify inbound webhook signatures from GitHub, Shopify, and Stripe with built-in HMAC helpers.
---

Requires the `webhook` feature:

```toml
[dependencies]
rust-web-server = { version = "17", features = ["webhook"] }
```

Any handler that accepts webhooks from a third party needs to confirm the request actually came from that provider — otherwise anyone who finds (or guesses) the endpoint URL can send a forged payload. `hmac` and `sha2` are already dependencies elsewhere in `rws` (`auth`, `crypto`, `storage-s3`); this feature just adds the provider-specific conventions on top so you don't re-derive them from each provider's docs.

## Quick start

```rust
use rust_web_server::webhook::{verify_webhook_signature, WebhookProvider};

fn handle_github_webhook(body: &[u8], signature_header: &str, secret: &[u8]) -> bool {
    verify_webhook_signature(WebhookProvider::GitHub, body, secret, signature_header)
}
```

:::caution[Use the raw request body]
`body` must be the exact bytes read off the wire. Re-serializing a parsed `Json<T>` value before verifying will not reproduce the byte sequence the provider actually signed, and verification will always fail. Read it via the `Body` extractor or `request.body` directly.
:::

## Supported providers

| Provider | Header | Format |
|---|---|---|
| GitHub | `X-Hub-Signature-256` | `sha256=<hex-hmac-sha256-of-body>` |
| Shopify | `X-Shopify-Hmac-Sha256` | `<base64-hmac-sha256-of-body>` |
| Stripe | `Stripe-Signature` | `t=<unix_ts>,v1=<hex>[,v1=<hex>...]` |

Call the per-provider function directly when the provider is known at the call site — this is equivalent to `verify_webhook_signature` with the matching `WebhookProvider` variant:

```rust
use rust_web_server::webhook::{verify_github_signature, verify_shopify_signature, verify_stripe_signature};

verify_github_signature(body, secret, header_value);
verify_shopify_signature(body, secret, header_value);
verify_stripe_signature(body, secret, header_value);
```

:::note[GitHub's legacy `X-Hub-Signature` header]
GitHub also sends an older SHA-1-based `X-Hub-Signature` for backward compatibility. `verify_github_signature` only supports `X-Hub-Signature-256` — SHA-1 is cryptographically broken and this crate has no SHA-1 dependency. Always prefer the SHA-256 header; GitHub recommends it for all new integrations.
:::

## Stripe's timestamp tolerance

Stripe signs `"{timestamp}.{body}"`, not the body alone, and expects you to reject requests whose timestamp is too far from the current time — this stops a captured request from being replayed later. `verify_stripe_signature` checks against a default 300-second (5-minute) window, matching Stripe's own recommendation:

```rust
use rust_web_server::webhook::{verify_stripe_signature_with_tolerance, STRIPE_DEFAULT_TOLERANCE_SECS};

// Widen the window to 10 minutes:
verify_stripe_signature_with_tolerance(body, secret, header_value, 600);

// The default constant, if you want to reference it:
assert_eq!(300, STRIPE_DEFAULT_TOLERANCE_SECS);
```

Stripe may list multiple `v1=` entries in the header while a signing-secret rotation is in progress — verification succeeds if *any* entry matches, so you don't need to special-case rotation.

## In a handler

```rust
use rust_web_server::app::App;
use rust_web_server::core::New;
use rust_web_server::request::Request;
use rust_web_server::response::{Response, STATUS_CODE_REASON_PHRASE};
use rust_web_server::webhook::{verify_webhook_signature, WebhookProvider};

fn stripe_webhook(request: &Request) -> Response {
    let signature = request
        .get_header("Stripe-Signature".to_string())
        .map(|h| h.value.clone())
        .unwrap_or_default();

    if !verify_webhook_signature(WebhookProvider::Stripe, &request.body, b"whsec_...", &signature) {
        let mut res = Response::new();
        res.status_code = *STATUS_CODE_REASON_PHRASE.n401_unauthorized.status_code;
        return res;
    }

    // Signature verified — safe to process request.body as a trusted Stripe event.
    Response::new()
}
```