Sample code for 30+ languages & platforms
PowerShell Requires Chilkat v11.5.0+

Box: Verify a Captured V2 Webhook Signature

See more Box Examples

Verify raw payload bytes and captured headers using the configured primary and secondary signing keys. The example rejects stale/future timestamps and compares randomized HMACs of the signatures to avoid exposing a signature-prefix timing comparison. It does not host an HTTP listener. The receiver must persist/deduplicate the verified event and acknowledge promptly.

See the Box API guide for setup and the complete workflow.

Chilkat PowerShell Downloads

PowerShell
Add-Type -Path "C:\chilkat\ChilkatDotNet47-x64\ChilkatDotNet47.dll"

$success = $false

#  Requires Chilkat 11.5.0 or later and a previously unlocked Chilkat bundle.
#  See Global Unlock Sample.
#  Guide: Box API Guide.

$success = $false

#  Save the exact HTTP body bytes to this file, without JSON reserialization.
$rawBody = New-Object Chilkat.BinData
$success = $rawBody.LoadFile("qa_data/box_webhook_body.bin")
if ($success -ne $true) {
    $($rawBody.LastErrorText)
    exit
}

#  Read these fields case-insensitively from the incoming request headers.
#  For this offline example, enter the captured header values.
$timestamp = "BOX_DELIVERY_TIMESTAMP"
$primarySignature = "BOX_SIGNATURE_PRIMARY"
$secondarySignature = "BOX_SIGNATURE_SECONDARY"
$signatureVersion = "1"
$signatureAlgorithm = "HmacSHA256"
#  Retrieve configured keys from protected application configuration.
$primaryKey = "BOX_PRIMARY_SIGNATURE_KEY"
$secondaryKey = "BOX_SECONDARY_SIGNATURE_KEY"
if (($signatureVersion -ne "1") -or ($signatureAlgorithm -ne "HmacSHA256")) {
    $("Unsupported signature version or algorithm.")
    exit
}

$delivered = New-Object Chilkat.CkDateTime
$success = $delivered.SetFromTimestamp($timestamp)
if ($success -ne $true) {
    $($delivered.LastErrorText)
    exit
}

$now = New-Object Chilkat.CkDateTime
$success = $now.SetFromCurrentSystemTime()
if ($success -ne $true) {
    $($now.LastErrorText)
    exit
}

$age = $now.GetAsUnixTime($false) - $delivered.GetAsUnixTime($false)
#  Box recommends rejecting deliveries older than ten minutes.
#  Allow at most 60 seconds of future clock skew; keep the server clock synchronized.
if (($age -gt 600) -or ($age -lt -60)) {
    $("Delivery timestamp is outside the accepted window.")
    exit
}

$signedBytes = New-Object Chilkat.BinData
$success = $signedBytes.AppendBd($rawBody)
if ($success -ne $true) {
    $($signedBytes.LastErrorText)
    exit
}

$success = $signedBytes.AppendString($timestamp,"utf-8")
if ($success -ne $true) {
    $($signedBytes.LastErrorText)
    exit
}

$mac = New-Object Chilkat.Crypt2
$mac.MacAlgorithm = "hmac"
$mac.HashAlgorithm = "sha256"
$mac.EncodingMode = "base64"
$success = $mac.SetMacKeyString($primaryKey)
if ($success -ne $true) {
    $($mac.LastErrorText)
    exit
}

$expectedPrimary = $mac.MacBdENC($signedBytes)
if ($mac.LastMethodSuccess -ne $true) {
    $($mac.LastErrorText)
    exit
}

$success = $mac.SetMacKeyString($secondaryKey)
if ($success -ne $true) {
    $($mac.LastErrorText)
    exit
}

$expectedSecondary = $mac.MacBdENC($signedBytes)
if ($mac.LastMethodSuccess -ne $true) {
    $($mac.LastErrorText)
    exit
}

#  Blind both sides with a fresh secret random key before equality comparison.
#  Never compare the original HMAC strings using a prefix-short-circuit comparison.
#  A host-language receiver can instead use its standard constant-time byte comparison.
$random = New-Object Chilkat.Prng
$comparisonKey = $random.GenRandom(32,"base64")
if ($random.LastMethodSuccess -ne $true) {
    $($random.LastErrorText)
    exit
}

$success = $mac.SetMacKeyEncoded($comparisonKey,"base64")
if ($success -ne $true) {
    $($mac.LastErrorText)
    exit
}

$pExpected = $mac.MacStringENC($expectedPrimary)
if ($mac.LastMethodSuccess -ne $true) {
    $($mac.LastErrorText)
    exit
}

$pReceived = $mac.MacStringENC($primarySignature)
if ($mac.LastMethodSuccess -ne $true) {
    $($mac.LastErrorText)
    exit
}

$sExpected = $mac.MacStringENC($expectedSecondary)
if ($mac.LastMethodSuccess -ne $true) {
    $($mac.LastErrorText)
    exit
}

$sReceived = $mac.MacStringENC($secondarySignature)
if ($mac.LastMethodSuccess -ne $true) {
    $($mac.LastErrorText)
    exit
}

$validPrimary = ($primaryKey -ne "") -and ($primarySignature -ne "") -and ($pExpected -eq $pReceived)
$validSecondary = ($secondaryKey -ne "") -and ($secondarySignature -ne "") -and ($sExpected -eq $sReceived)
if (($validPrimary -eq $false) -and ($validSecondary -eq $false)) {
    $("Signature verification failed.")
    exit
}

#  Parse JSON only after signature verification.
$webhookEvent = New-Object Chilkat.JsonObject
$success = $webhookEvent.Load($rawBody.GetString("utf-8"))
if ($success -ne $true) {
    $($webhookEvent.LastErrorText)
    exit
}

$("Verified event ID: " + $webhookEvent.StringOf("id"))
$("Trigger: " + $webhookEvent.StringOf("trigger"))
#  Deduplicate using event.id, not BOX-DELIVERY-ID, which changes on retries.
#  Verify the target belongs to your configured webhook before dispatching work.