PowerShell 7: verify a Sume webhook with HMACSHA256, constant-time
A PowerShell function that refuses an empty secret, checks the 300-second window and accepts any entry in a rotated sume-v1 header. Under 30 lines.

In PowerShell 7 you can verify a Sume webhook with .NET's HMACSHA256 and CryptographicOperations.FixedTimeEquals. Build the message as <timestamp>.<raw body bytes>, compute the HMAC, prefix the hex with sume-v1=, and compare it with every comma-separated entry of x-sume-webhook-signature.
The scheme
The values below come from the Sume webhook docs. The key point for a script is to hash the body bytes exactly as received, not a re-serialized object.
| Input | Value |
|---|---|
| Message | <x-sume-webhook-timestamp>.<raw body> |
| Algorithm | HMAC-SHA256, hex output |
| Expected header entry | sume-v1=<hex> |
| During rotation | comma-separated entries, newest first, for 24 hours |
| Replay window | 300 seconds (the docs call five minutes a reasonable default) |
| Secret | SUME_COM_WEBHOOK_SIGNING_SECRET, from GET /v1/webhooks/signing-secret |
The function
The function throws on an empty secret instead of returning false, because an empty secret means the receiver is misconfigured and would otherwise pass every forged request into a different code path. It returns $false for a bad timestamp, an old timestamp or a wrong signature. It checks every header entry and does not stop at the first match, so timing does not reveal which entry matched.
function Test-SumeSignature {
param([byte[]]$Body, [string]$Timestamp, [string]$Header,
[string]$Secret, [int]$Tolerance = 300)
if ([string]::IsNullOrEmpty($Secret)) { throw "empty webhook secret" }
$ts = 0L
if (-not [long]::TryParse($Timestamp, [ref]$ts)) { return $false }
$now = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
if ([math]::Abs($now - $ts) -gt $Tolerance) { return $false }
$key = [Text.Encoding]::UTF8.GetBytes($Secret)
$msg = [Text.Encoding]::UTF8.GetBytes("$Timestamp.") + $Body
$mac = [Security.Cryptography.HMACSHA256]::new($key)
$hex = [Convert]::ToHexString($mac.ComputeHash($msg)).ToLowerInvariant()
$want = [Text.Encoding]::UTF8.GetBytes("sume-v1=$hex")
$ok = $false
foreach ($entry in $Header.Split(",")) {
$got = [Text.Encoding]::UTF8.GetBytes($entry.Trim())
if ($got.Length -eq $want.Length -and
[Security.Cryptography.CryptographicOperations]::FixedTimeEquals($got, $want)) {
$ok = $true
}
}
return $ok
}Edge cases the function covers
A missing x-sume-webhook-timestamp header arrives as an empty string, which fails TryParse and returns $false. A header with a stray space after the comma still verifies, because each entry is trimmed. An entry that does not start with sume-v1= can never equal the expected bytes, so it is ignored without a special case.
A timestamp from the future by more than 300 seconds fails the same check as one from the past, because the code uses the absolute difference. If your server clock drifts, fix the clock; do not widen the window to hide it.
Using it
Feed it the request body as bytes. With an HttpListener, copy request.InputStream into a MemoryStream and call ToArray(); do not read it as a string and re-encode it. Return a 2xx quickly after you write the job_id somewhere durable, because Sume waits 10 seconds per attempt and retries a non-2xx answer up to 10 times.
Test it with a body you sign yourself, then flip one byte. For a longer scripted check without PowerShell, see signing a fake delivery with openssl and curl.
Convert.ToHexStringneeds PowerShell 7 (.NET 5 or later).- Do not use
-eqon the signature strings: it is not constant-time. - Read the secret from an environment variable, not from the script.
Sources
Related posts
More in Developers
- PowerShell: Invoke-RestMethod for a 30-second Wan 3.0 clip
Windows PowerShell script that posts a 30-second wan-3.0 job, loops until it completes and saves the MP4 with Invoke-WebRequest. $3.75 at 720p.
- Pre-flight an Omni Flash request against the Sume model catalog
A short Python check that reads supported durations, resolutions and aspect ratios for gemini-omni-flash-1.1 via /v1/videos/models.
- Preflight a 3-minute Short with Timeline plan before you pay
POST /v1/timeline-1.0/plan compiles a Short without a job or a reserve and returns billable minutes and the estimate. A runnable Python check for six slots.
- Promise.allSettled for many Sume job statuses: isolate one failure
Check many Sume job statuses at once with Promise.allSettled so one 429 or network error is reported on its own row and does not hide the other results.
Written by Sume