PureBasic Requires Chilkat v11.5.0+
PureBasic
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 PureBasic Downloads
IncludeFile "CkBinData.pb"
IncludeFile "CkJsonObject.pb"
IncludeFile "CkPrng.pb"
IncludeFile "CkDateTime.pb"
IncludeFile "CkCrypt2.pb"
Procedure ChilkatExample()
success.i = 0
; Requires Chilkat 11.5.0 or later and a previously unlocked Chilkat bundle.
; See Global Unlock Sample.
; Guide: Box API Guide.
success = 0
; Save the exact HTTP body bytes to this file, without JSON reserialization.
rawBody.i = CkBinData::ckCreate()
If rawBody.i = 0
Debug "Failed to create object."
ProcedureReturn
EndIf
success = CkBinData::ckLoadFile(rawBody,"qa_data/box_webhook_body.bin")
If success <> 1
Debug CkBinData::ckLastErrorText(rawBody)
CkBinData::ckDispose(rawBody)
ProcedureReturn
EndIf
; Read these fields case-insensitively from the incoming request headers.
; For this offline example, enter the captured header values.
timestamp.s = "BOX_DELIVERY_TIMESTAMP"
primarySignature.s = "BOX_SIGNATURE_PRIMARY"
secondarySignature.s = "BOX_SIGNATURE_SECONDARY"
signatureVersion.s = "1"
signatureAlgorithm.s = "HmacSHA256"
; Retrieve configured keys from protected application configuration.
primaryKey.s = "BOX_PRIMARY_SIGNATURE_KEY"
secondaryKey.s = "BOX_SECONDARY_SIGNATURE_KEY"
If (signatureVersion <> "1") OR (signatureAlgorithm <> "HmacSHA256")
Debug "Unsupported signature version or algorithm."
CkBinData::ckDispose(rawBody)
ProcedureReturn
EndIf
delivered.i = CkDateTime::ckCreate()
If delivered.i = 0
Debug "Failed to create object."
ProcedureReturn
EndIf
success = CkDateTime::ckSetFromTimestamp(delivered,timestamp)
If success <> 1
Debug CkDateTime::ckLastErrorText(delivered)
CkBinData::ckDispose(rawBody)
CkDateTime::ckDispose(delivered)
ProcedureReturn
EndIf
now.i = CkDateTime::ckCreate()
If now.i = 0
Debug "Failed to create object."
ProcedureReturn
EndIf
success = CkDateTime::ckSetFromCurrentSystemTime(now)
If success <> 1
Debug CkDateTime::ckLastErrorText(now)
CkBinData::ckDispose(rawBody)
CkDateTime::ckDispose(delivered)
CkDateTime::ckDispose(now)
ProcedureReturn
EndIf
age.i = Val(CkDateTime::ckGetAsUnixTime(now,0)) - Val(CkDateTime::ckGetAsUnixTime(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 (age > 600) OR (age < -60)
Debug "Delivery timestamp is outside the accepted window."
CkBinData::ckDispose(rawBody)
CkDateTime::ckDispose(delivered)
CkDateTime::ckDispose(now)
ProcedureReturn
EndIf
signedBytes.i = CkBinData::ckCreate()
If signedBytes.i = 0
Debug "Failed to create object."
ProcedureReturn
EndIf
success = CkBinData::ckAppendBd(signedBytes,rawBody)
If success <> 1
Debug CkBinData::ckLastErrorText(signedBytes)
CkBinData::ckDispose(rawBody)
CkDateTime::ckDispose(delivered)
CkDateTime::ckDispose(now)
CkBinData::ckDispose(signedBytes)
ProcedureReturn
EndIf
success = CkBinData::ckAppendString(signedBytes,timestamp,"utf-8")
If success <> 1
Debug CkBinData::ckLastErrorText(signedBytes)
CkBinData::ckDispose(rawBody)
CkDateTime::ckDispose(delivered)
CkDateTime::ckDispose(now)
CkBinData::ckDispose(signedBytes)
ProcedureReturn
EndIf
mac.i = CkCrypt2::ckCreate()
If mac.i = 0
Debug "Failed to create object."
ProcedureReturn
EndIf
CkCrypt2::setCkMacAlgorithm(mac, "hmac")
CkCrypt2::setCkHashAlgorithm(mac, "sha256")
CkCrypt2::setCkEncodingMode(mac, "base64")
success = CkCrypt2::ckSetMacKeyString(mac,primaryKey)
If success <> 1
Debug CkCrypt2::ckLastErrorText(mac)
CkBinData::ckDispose(rawBody)
CkDateTime::ckDispose(delivered)
CkDateTime::ckDispose(now)
CkBinData::ckDispose(signedBytes)
CkCrypt2::ckDispose(mac)
ProcedureReturn
EndIf
expectedPrimary.s = CkCrypt2::ckMacBdENC(mac,signedBytes)
If CkCrypt2::ckLastMethodSuccess(mac) <> 1
Debug CkCrypt2::ckLastErrorText(mac)
CkBinData::ckDispose(rawBody)
CkDateTime::ckDispose(delivered)
CkDateTime::ckDispose(now)
CkBinData::ckDispose(signedBytes)
CkCrypt2::ckDispose(mac)
ProcedureReturn
EndIf
success = CkCrypt2::ckSetMacKeyString(mac,secondaryKey)
If success <> 1
Debug CkCrypt2::ckLastErrorText(mac)
CkBinData::ckDispose(rawBody)
CkDateTime::ckDispose(delivered)
CkDateTime::ckDispose(now)
CkBinData::ckDispose(signedBytes)
CkCrypt2::ckDispose(mac)
ProcedureReturn
EndIf
expectedSecondary.s = CkCrypt2::ckMacBdENC(mac,signedBytes)
If CkCrypt2::ckLastMethodSuccess(mac) <> 1
Debug CkCrypt2::ckLastErrorText(mac)
CkBinData::ckDispose(rawBody)
CkDateTime::ckDispose(delivered)
CkDateTime::ckDispose(now)
CkBinData::ckDispose(signedBytes)
CkCrypt2::ckDispose(mac)
ProcedureReturn
EndIf
; 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.i = CkPrng::ckCreate()
If random.i = 0
Debug "Failed to create object."
ProcedureReturn
EndIf
comparisonKey.s = CkPrng::ckGenRandom(random,32,"base64")
If CkPrng::ckLastMethodSuccess(random) <> 1
Debug CkPrng::ckLastErrorText(random)
CkBinData::ckDispose(rawBody)
CkDateTime::ckDispose(delivered)
CkDateTime::ckDispose(now)
CkBinData::ckDispose(signedBytes)
CkCrypt2::ckDispose(mac)
CkPrng::ckDispose(random)
ProcedureReturn
EndIf
success = CkCrypt2::ckSetMacKeyEncoded(mac,comparisonKey,"base64")
If success <> 1
Debug CkCrypt2::ckLastErrorText(mac)
CkBinData::ckDispose(rawBody)
CkDateTime::ckDispose(delivered)
CkDateTime::ckDispose(now)
CkBinData::ckDispose(signedBytes)
CkCrypt2::ckDispose(mac)
CkPrng::ckDispose(random)
ProcedureReturn
EndIf
pExpected.s = CkCrypt2::ckMacStringENC(mac,expectedPrimary)
If CkCrypt2::ckLastMethodSuccess(mac) <> 1
Debug CkCrypt2::ckLastErrorText(mac)
CkBinData::ckDispose(rawBody)
CkDateTime::ckDispose(delivered)
CkDateTime::ckDispose(now)
CkBinData::ckDispose(signedBytes)
CkCrypt2::ckDispose(mac)
CkPrng::ckDispose(random)
ProcedureReturn
EndIf
pReceived.s = CkCrypt2::ckMacStringENC(mac,primarySignature)
If CkCrypt2::ckLastMethodSuccess(mac) <> 1
Debug CkCrypt2::ckLastErrorText(mac)
CkBinData::ckDispose(rawBody)
CkDateTime::ckDispose(delivered)
CkDateTime::ckDispose(now)
CkBinData::ckDispose(signedBytes)
CkCrypt2::ckDispose(mac)
CkPrng::ckDispose(random)
ProcedureReturn
EndIf
sExpected.s = CkCrypt2::ckMacStringENC(mac,expectedSecondary)
If CkCrypt2::ckLastMethodSuccess(mac) <> 1
Debug CkCrypt2::ckLastErrorText(mac)
CkBinData::ckDispose(rawBody)
CkDateTime::ckDispose(delivered)
CkDateTime::ckDispose(now)
CkBinData::ckDispose(signedBytes)
CkCrypt2::ckDispose(mac)
CkPrng::ckDispose(random)
ProcedureReturn
EndIf
sReceived.s = CkCrypt2::ckMacStringENC(mac,secondarySignature)
If CkCrypt2::ckLastMethodSuccess(mac) <> 1
Debug CkCrypt2::ckLastErrorText(mac)
CkBinData::ckDispose(rawBody)
CkDateTime::ckDispose(delivered)
CkDateTime::ckDispose(now)
CkBinData::ckDispose(signedBytes)
CkCrypt2::ckDispose(mac)
CkPrng::ckDispose(random)
ProcedureReturn
EndIf
validPrimary.i = (primaryKey <> "") AND (primarySignature <> "") AND (pExpected = pReceived)
validSecondary.i = (secondaryKey <> "") AND (secondarySignature <> "") AND (sExpected = sReceived)
If (validPrimary = 0) AND (validSecondary = 0)
Debug "Signature verification failed."
CkBinData::ckDispose(rawBody)
CkDateTime::ckDispose(delivered)
CkDateTime::ckDispose(now)
CkBinData::ckDispose(signedBytes)
CkCrypt2::ckDispose(mac)
CkPrng::ckDispose(random)
ProcedureReturn
EndIf
; Parse JSON only after signature verification.
webhookEvent.i = CkJsonObject::ckCreate()
If webhookEvent.i = 0
Debug "Failed to create object."
ProcedureReturn
EndIf
success = CkJsonObject::ckLoad(webhookEvent,CkBinData::ckGetString(rawBody,"utf-8"))
If success <> 1
Debug CkJsonObject::ckLastErrorText(webhookEvent)
CkBinData::ckDispose(rawBody)
CkDateTime::ckDispose(delivered)
CkDateTime::ckDispose(now)
CkBinData::ckDispose(signedBytes)
CkCrypt2::ckDispose(mac)
CkPrng::ckDispose(random)
CkJsonObject::ckDispose(webhookEvent)
ProcedureReturn
EndIf
Debug "Verified event ID: " + CkJsonObject::ckStringOf(webhookEvent,"id")
Debug "Trigger: " + CkJsonObject::ckStringOf(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.
CkBinData::ckDispose(rawBody)
CkDateTime::ckDispose(delivered)
CkDateTime::ckDispose(now)
CkBinData::ckDispose(signedBytes)
CkCrypt2::ckDispose(mac)
CkPrng::ckDispose(random)
CkJsonObject::ckDispose(webhookEvent)
ProcedureReturn
EndProcedure