Sample code for 30+ languages & platforms
Tcl 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 Tcl Downloads

Tcl

load ./chilkat.dll

set success 0

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

set success 0

#  Save the exact HTTP body bytes to this file, without JSON reserialization.
set rawBody [new_CkBinData]

set success [CkBinData_LoadFile $rawBody "qa_data/box_webhook_body.bin"]
if {$success != 1} then {
    puts [CkBinData_lastErrorText $rawBody]
    delete_CkBinData $rawBody
    exit
}

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

set delivered [new_CkDateTime]

set success [CkDateTime_SetFromTimestamp $delivered $timestamp]
if {$success != 1} then {
    puts [CkDateTime_lastErrorText $delivered]
    delete_CkBinData $rawBody
    delete_CkDateTime $delivered
    exit
}

set now [new_CkDateTime]

set success [CkDateTime_SetFromCurrentSystemTime $now]
if {$success != 1} then {
    puts [CkDateTime_lastErrorText $now]
    delete_CkBinData $rawBody
    delete_CkDateTime $delivered
    delete_CkDateTime $now
    exit
}

set age [expr [CkDateTime_GetAsUnixTime $now 0] - [CkDateTime_GetAsUnixTime $delivered 0]]
#  Box recommends rejecting deliveries older than ten minutes.
#  Allow at most 60 seconds of future clock skew; keep the server clock synchronized.
if {expr [$age > 600]  ||  [$age < -60]} then {
    puts "Delivery timestamp is outside the accepted window."
    delete_CkBinData $rawBody
    delete_CkDateTime $delivered
    delete_CkDateTime $now
    exit
}

set signedBytes [new_CkBinData]

set success [CkBinData_AppendBd $signedBytes $rawBody]
if {$success != 1} then {
    puts [CkBinData_lastErrorText $signedBytes]
    delete_CkBinData $rawBody
    delete_CkDateTime $delivered
    delete_CkDateTime $now
    delete_CkBinData $signedBytes
    exit
}

set success [CkBinData_AppendString $signedBytes $timestamp "utf-8"]
if {$success != 1} then {
    puts [CkBinData_lastErrorText $signedBytes]
    delete_CkBinData $rawBody
    delete_CkDateTime $delivered
    delete_CkDateTime $now
    delete_CkBinData $signedBytes
    exit
}

set mac [new_CkCrypt2]

CkCrypt2_put_MacAlgorithm $mac "hmac"
CkCrypt2_put_HashAlgorithm $mac "sha256"
CkCrypt2_put_EncodingMode $mac "base64"
set success [CkCrypt2_SetMacKeyString $mac $primaryKey]
if {$success != 1} then {
    puts [CkCrypt2_lastErrorText $mac]
    delete_CkBinData $rawBody
    delete_CkDateTime $delivered
    delete_CkDateTime $now
    delete_CkBinData $signedBytes
    delete_CkCrypt2 $mac
    exit
}

set expectedPrimary [CkCrypt2_macBdENC $mac $signedBytes]
if {[CkCrypt2_get_LastMethodSuccess $mac] != 1} then {
    puts [CkCrypt2_lastErrorText $mac]
    delete_CkBinData $rawBody
    delete_CkDateTime $delivered
    delete_CkDateTime $now
    delete_CkBinData $signedBytes
    delete_CkCrypt2 $mac
    exit
}

set success [CkCrypt2_SetMacKeyString $mac $secondaryKey]
if {$success != 1} then {
    puts [CkCrypt2_lastErrorText $mac]
    delete_CkBinData $rawBody
    delete_CkDateTime $delivered
    delete_CkDateTime $now
    delete_CkBinData $signedBytes
    delete_CkCrypt2 $mac
    exit
}

set expectedSecondary [CkCrypt2_macBdENC $mac $signedBytes]
if {[CkCrypt2_get_LastMethodSuccess $mac] != 1} then {
    puts [CkCrypt2_lastErrorText $mac]
    delete_CkBinData $rawBody
    delete_CkDateTime $delivered
    delete_CkDateTime $now
    delete_CkBinData $signedBytes
    delete_CkCrypt2 $mac
    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.
set random [new_CkPrng]

set comparisonKey [CkPrng_genRandom $random 32 "base64"]
if {[CkPrng_get_LastMethodSuccess $random] != 1} then {
    puts [CkPrng_lastErrorText $random]
    delete_CkBinData $rawBody
    delete_CkDateTime $delivered
    delete_CkDateTime $now
    delete_CkBinData $signedBytes
    delete_CkCrypt2 $mac
    delete_CkPrng $random
    exit
}

set success [CkCrypt2_SetMacKeyEncoded $mac $comparisonKey "base64"]
if {$success != 1} then {
    puts [CkCrypt2_lastErrorText $mac]
    delete_CkBinData $rawBody
    delete_CkDateTime $delivered
    delete_CkDateTime $now
    delete_CkBinData $signedBytes
    delete_CkCrypt2 $mac
    delete_CkPrng $random
    exit
}

set pExpected [CkCrypt2_macStringENC $mac $expectedPrimary]
if {[CkCrypt2_get_LastMethodSuccess $mac] != 1} then {
    puts [CkCrypt2_lastErrorText $mac]
    delete_CkBinData $rawBody
    delete_CkDateTime $delivered
    delete_CkDateTime $now
    delete_CkBinData $signedBytes
    delete_CkCrypt2 $mac
    delete_CkPrng $random
    exit
}

set pReceived [CkCrypt2_macStringENC $mac $primarySignature]
if {[CkCrypt2_get_LastMethodSuccess $mac] != 1} then {
    puts [CkCrypt2_lastErrorText $mac]
    delete_CkBinData $rawBody
    delete_CkDateTime $delivered
    delete_CkDateTime $now
    delete_CkBinData $signedBytes
    delete_CkCrypt2 $mac
    delete_CkPrng $random
    exit
}

set sExpected [CkCrypt2_macStringENC $mac $expectedSecondary]
if {[CkCrypt2_get_LastMethodSuccess $mac] != 1} then {
    puts [CkCrypt2_lastErrorText $mac]
    delete_CkBinData $rawBody
    delete_CkDateTime $delivered
    delete_CkDateTime $now
    delete_CkBinData $signedBytes
    delete_CkCrypt2 $mac
    delete_CkPrng $random
    exit
}

set sReceived [CkCrypt2_macStringENC $mac $secondarySignature]
if {[CkCrypt2_get_LastMethodSuccess $mac] != 1} then {
    puts [CkCrypt2_lastErrorText $mac]
    delete_CkBinData $rawBody
    delete_CkDateTime $delivered
    delete_CkDateTime $now
    delete_CkBinData $signedBytes
    delete_CkCrypt2 $mac
    delete_CkPrng $random
    exit
}

set validPrimary [expr [$primaryKey != ""]  &&  [expr [$primarySignature != ""]  &&  [$pExpected == $pReceived]]]
set validSecondary [expr [$secondaryKey != ""]  &&  [expr [$secondarySignature != ""]  &&  [$sExpected == $sReceived]]]
if {expr [$validPrimary == 0]  &&  [$validSecondary == 0]} then {
    puts "Signature verification failed."
    delete_CkBinData $rawBody
    delete_CkDateTime $delivered
    delete_CkDateTime $now
    delete_CkBinData $signedBytes
    delete_CkCrypt2 $mac
    delete_CkPrng $random
    exit
}

#  Parse JSON only after signature verification.
set webhookEvent [new_CkJsonObject]

set success [CkJsonObject_Load $webhookEvent [CkBinData_getString $rawBody "utf-8"]]
if {$success != 1} then {
    puts [CkJsonObject_lastErrorText $webhookEvent]
    delete_CkBinData $rawBody
    delete_CkDateTime $delivered
    delete_CkDateTime $now
    delete_CkBinData $signedBytes
    delete_CkCrypt2 $mac
    delete_CkPrng $random
    delete_CkJsonObject $webhookEvent
    exit
}

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

delete_CkBinData $rawBody
delete_CkDateTime $delivered
delete_CkDateTime $now
delete_CkBinData $signedBytes
delete_CkCrypt2 $mac
delete_CkPrng $random
delete_CkJsonObject $webhookEvent