Lua Requires Chilkat v11.5.0+
Lua
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 Lua Downloads
local chilkat = require("chilkat")
-- Requires Chilkat 11.5.0 or later and a previously unlocked Chilkat bundle.
-- See Global Unlock Sample.
-- Guide: Box API Guide.
local success = false
-- Save the exact HTTP body bytes to this file, without JSON reserialization.
local rawBody = chilkat.BinData.new()
success = rawBody:load_file("qa_data/box_webhook_body.bin")
if not success then
print(rawBody.last_error_text)
return
end
-- Read these fields case-insensitively from the incoming request headers.
-- For this offline example, enter the captured header values.
local timestamp = "BOX_DELIVERY_TIMESTAMP"
local primarySignature = "BOX_SIGNATURE_PRIMARY"
local secondarySignature = "BOX_SIGNATURE_SECONDARY"
local signatureVersion = "1"
local signatureAlgorithm = "HmacSHA256"
-- Retrieve configured keys from protected application configuration.
local primaryKey = "BOX_PRIMARY_SIGNATURE_KEY"
local secondaryKey = "BOX_SECONDARY_SIGNATURE_KEY"
if (signatureVersion ~= "1") or (signatureAlgorithm ~= "HmacSHA256") then
print("Unsupported signature version or algorithm.")
return
end
local delivered = chilkat.DateTime.new()
success = delivered:set_from_timestamp(timestamp)
if not success then
print(delivered.last_error_text)
return
end
local now = chilkat.DateTime.new()
success = now:set_from_current_system_time()
if not success then
print(now.last_error_text)
return
end
local age = tonumber(now:get_as_unix_time(false)) - tonumber(delivered:get_as_unix_time(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 > 600) or (age < (-60)) then
print("Delivery timestamp is outside the accepted window.")
return
end
local signedBytes = chilkat.BinData.new()
success = signedBytes:append_bd(rawBody)
if not success then
print(signedBytes.last_error_text)
return
end
success = signedBytes:append_string(timestamp, "utf-8")
if not success then
print(signedBytes.last_error_text)
return
end
local mac = chilkat.Crypt2.new()
mac.mac_algorithm = "hmac"
mac.hash_algorithm = "sha256"
mac.encoding_mode = "base64"
success = mac:set_mac_key_string(primaryKey)
if not success then
print(mac.last_error_text)
return
end
local expectedPrimary = mac:mac_bd_enc(signedBytes)
if not mac.last_method_success then
print(mac.last_error_text)
return
end
success = mac:set_mac_key_string(secondaryKey)
if not success then
print(mac.last_error_text)
return
end
local expectedSecondary = mac:mac_bd_enc(signedBytes)
if not mac.last_method_success then
print(mac.last_error_text)
return
end
-- 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.
local random = chilkat.Prng.new()
local comparisonKey = random:gen_random(32, "base64")
if not random.last_method_success then
print(random.last_error_text)
return
end
success = mac:set_mac_key_encoded(comparisonKey, "base64")
if not success then
print(mac.last_error_text)
return
end
local pExpected = mac:mac_string_enc(expectedPrimary)
if not mac.last_method_success then
print(mac.last_error_text)
return
end
local pReceived = mac:mac_string_enc(primarySignature)
if not mac.last_method_success then
print(mac.last_error_text)
return
end
local sExpected = mac:mac_string_enc(expectedSecondary)
if not mac.last_method_success then
print(mac.last_error_text)
return
end
local sReceived = mac:mac_string_enc(secondarySignature)
if not mac.last_method_success then
print(mac.last_error_text)
return
end
local validPrimary = (primaryKey ~= "") and ((primarySignature ~= "") and (pExpected == pReceived))
local validSecondary = (secondaryKey ~= "") and ((secondarySignature ~= "") and (sExpected == sReceived))
if (not validPrimary) and (not validSecondary) then
print("Signature verification failed.")
return
end
-- Parse JSON only after signature verification.
local webhookEvent = chilkat.JsonObject.new()
success = webhookEvent:load(rawBody:get_string("utf-8"))
if not success then
print(webhookEvent.last_error_text)
return
end
print("Verified event ID: " .. webhookEvent:string_of("id"))
print("Trigger: " .. webhookEvent:string_of("trigger"))
-- Deduplicate using event.id, not BOX-DELIVERY-ID, which changes on retries.
-- Verify the target belongs to your configured webhook before dispatching work.