Skip to main content

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.

The signature is read from the X-Hub-Signature header.

X-Hub-Signature: t=1777036800,v1=4f2b...
ComponentDescription
tUnix timestamp, in seconds.
v1HMAC-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:

  1. Reads X-Hub-Signature.
  2. Parses exactly one t timestamp and one or more v1 signatures.
  3. Rejects missing, malformed, duplicate timestamp, non-hex, or wrong-length signature values.
  4. Rejects timestamps outside the configured tolerance.
  5. Reads the exact raw request body bytes.
  6. Builds the signed payload as timestamp + "." + rawBody.
  7. Computes HMAC-SHA256 with the webhook secret as the key.
  8. Compares the computed signature against every v1 signature with constant-time comparison.

Defaults

SettingDefault
HeaderX-Hub-Signature
Signature componentv1
Timestamp componentt
Timestamp tolerance300 seconds
Maximum request body size5 MiB
HMAC algorithmHMAC-SHA256

SDK Support

SDKWebhook signature helper
.NETGuardhouseWebhookSignatureValidator
JavaScript / Node.jsNo helper documented in this page. Follow the validation process above.
PythonNo 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-SHA256 is 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 v1 signatures allow secret rotation patterns where more than one valid signature may be present.
  • The 5 MiB body limit prevents the validator from buffering unbounded request bodies.