Tcl Requires Chilkat v11.5.0+
Tcl
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
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