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

PureBasic
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