Signature Validation
Guardhouse signs webhook deliveries with an HMAC signature in the X-Hub-Signature header.
Validate the signature before parsing or processing the event body.
Header
The signature is read from the X-Hub-Signature header.
X-Hub-Signature: t=1777036800,v1=4f2b...
| Component | Description |
|---|---|
t | Unix timestamp, in seconds. |
v1 | HMAC-SHA256 signature encoded as a 64-character hex value. More than one v1 value can be present. |
Validation Process
Every implementation should perform these checks:
- Reads
X-Hub-Signature. - Parses exactly one
ttimestamp and one or morev1signatures. - Rejects missing, malformed, duplicate timestamp, non-hex, or wrong-length signature values.
- Rejects timestamps outside the configured tolerance.
- Reads the exact raw request body bytes.
- Builds the signed payload as
timestamp + "." + rawBody. - Computes
HMAC-SHA256with the webhook secret as the key. - Compares the computed signature against every
v1signature with constant-time comparison.
Defaults
| Setting | Default |
|---|---|
| Header | X-Hub-Signature |
| Signature component | v1 |
| Timestamp component | t |
| Timestamp tolerance | 300 seconds |
| Maximum request body size | 5 MiB |
| HMAC algorithm | HMAC-SHA256 |
SDK Support
| SDK | Webhook signature helper |
|---|---|
| .NET | GuardhouseWebhookSignatureValidator |
| JavaScript / Node.js | No helper documented in this page. Follow the validation process above. |
| Python | No helper documented in this page. Follow the validation process above. |
.NET SDK Example
using Guardhouse.SDK.Webhooks;
app.MapPost("/webhooks/guardhouse", async (HttpRequest request, IConfiguration configuration) =>
{
var secret = configuration["Guardhouse:Webhooks:Secret"];
if (string.IsNullOrWhiteSpace(secret))
{
return Results.StatusCode(StatusCodes.Status500InternalServerError);
}
var isValid = await GuardhouseWebhookSignatureValidator.IsValidAsync(request, secret);
if (!isValid)
{
return Results.BadRequest("Signature verification failed.");
}
using var reader = new StreamReader(request.Body);
var json = await reader.ReadToEndAsync();
// Deserialize and handle the webhook envelope here.
return Results.Ok();
});
IsValidAsync validates the request body and rewinds the ASP.NET Core request body stream, so the same body can be read after validation.
Byte Secret Overload
For stricter secret-memory handling in .NET, use the overload that accepts secret bytes and clear the byte array after validation.
using System.Security.Cryptography;
using System.Text;
using Guardhouse.SDK.Webhooks;
var secretBytes = Encoding.UTF8.GetBytes(configuration["Guardhouse:Webhooks:Secret"]!);
try
{
var isValid = await GuardhouseWebhookSignatureValidator.IsValidAsync(request, secretBytes);
}
finally
{
CryptographicOperations.ZeroMemory(secretBytes);
}
Security Rationale
HMAC-SHA256is a standard keyed message authentication code. It proves that the sender knows the webhook secret and that the body was not changed in transit.- The timestamp is included in the signed payload to reduce replay risk. A copied request outside the tolerance window is rejected.
- The raw request body is signed instead of parsed JSON so whitespace, ordering, encoding, and parser behavior cannot change what is verified.
- Constant-time comparison reduces timing side channels when comparing the expected signature with attacker-controlled input.
- Multiple
v1signatures allow secret rotation patterns where more than one valid signature may be present. - The
5 MiBbody limit prevents the validator from buffering unbounded request bodies.