Skip to content

Webhook Signature Verification

Requires the webhook feature:

[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

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)
}

Supported providers

ProviderHeaderFormat
GitHubX-Hub-Signature-256sha256=<hex-hmac-sha256-of-body>
ShopifyX-Shopify-Hmac-Sha256<base64-hmac-sha256-of-body>
StripeStripe-Signaturet=<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:

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);

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:

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

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()
}