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

Lua
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.