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

Swift

func chilkatTest() {
    var success: Bool = false

    //  Requires Chilkat 11.5.0 or later and a previously unlocked Chilkat bundle.
    //  See Global Unlock Sample.
    //  Guide: Box API Guide.

    success = false

    //  Save the exact HTTP body bytes to this file, without JSON reserialization.
    let rawBody = CkoBinData()!
    success = rawBody.loadFile(path: "qa_data/box_webhook_body.bin")
    if success != true {
        print("\(rawBody.lastErrorText!)")
        return
    }

    //  Read these fields case-insensitively from the incoming request headers.
    //  For this offline example, enter the captured header values.
    var timestamp: String? = "BOX_DELIVERY_TIMESTAMP"
    var primarySignature: String? = "BOX_SIGNATURE_PRIMARY"
    var secondarySignature: String? = "BOX_SIGNATURE_SECONDARY"
    var signatureVersion: String? = "1"
    var signatureAlgorithm: String? = "HmacSHA256"
    //  Retrieve configured keys from protected application configuration.
    var primaryKey: String? = "BOX_PRIMARY_SIGNATURE_KEY"
    var secondaryKey: String? = "BOX_SECONDARY_SIGNATURE_KEY"
    if (signatureVersion != "1") || (signatureAlgorithm != "HmacSHA256") {
        print("Unsupported signature version or algorithm.")
        return
    }

    let delivered = CkoDateTime()!
    success = delivered.set(fromTimestamp: timestamp)
    if success != true {
        print("\(delivered.lastErrorText!)")
        return
    }

    let now = CkoDateTime()!
    success = now.setFromCurrentSystemTime()
    if success != true {
        print("\(now.lastErrorText!)")
        return
    }

    var age: Int = [now.get(asUnixTime: false) intValue] - [delivered.get(asUnixTime: false) intValue]
    //  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) || (age < -60) {
        print("Delivery timestamp is outside the accepted window.")
        return
    }

    let signedBytes = CkoBinData()!
    success = signedBytes.appendBd(binData: rawBody)
    if success != true {
        print("\(signedBytes.lastErrorText!)")
        return
    }

    success = signedBytes.appendString(str: timestamp, charset: "utf-8")
    if success != true {
        print("\(signedBytes.lastErrorText!)")
        return
    }

    let mac = CkoCrypt2()!
    mac.macAlgorithm = "hmac"
    mac.hashAlgorithm = "sha256"
    mac.encodingMode = "base64"
    success = mac.setMacKeyString(key: primaryKey)
    if success != true {
        print("\(mac.lastErrorText!)")
        return
    }

    var expectedPrimary: String? = mac.macBdENC(bd: signedBytes)
    if mac.lastMethodSuccess != true {
        print("\(mac.lastErrorText!)")
        return
    }

    success = mac.setMacKeyString(key: secondaryKey)
    if success != true {
        print("\(mac.lastErrorText!)")
        return
    }

    var expectedSecondary: String? = mac.macBdENC(bd: signedBytes)
    if mac.lastMethodSuccess != true {
        print("\(mac.lastErrorText!)")
        return
    }

    //  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.
    let random = CkoPrng()!
    var comparisonKey: String? = random.genRandom(numBytes: 32, encoding: "base64")
    if random.lastMethodSuccess != true {
        print("\(random.lastErrorText!)")
        return
    }

    success = mac.setMacKeyEncoded(key: comparisonKey, encoding: "base64")
    if success != true {
        print("\(mac.lastErrorText!)")
        return
    }

    var pExpected: String? = mac.macStringENC(inText: expectedPrimary)
    if mac.lastMethodSuccess != true {
        print("\(mac.lastErrorText!)")
        return
    }

    var pReceived: String? = mac.macStringENC(inText: primarySignature)
    if mac.lastMethodSuccess != true {
        print("\(mac.lastErrorText!)")
        return
    }

    var sExpected: String? = mac.macStringENC(inText: expectedSecondary)
    if mac.lastMethodSuccess != true {
        print("\(mac.lastErrorText!)")
        return
    }

    var sReceived: String? = mac.macStringENC(inText: secondarySignature)
    if mac.lastMethodSuccess != true {
        print("\(mac.lastErrorText!)")
        return
    }

    var validPrimary: Bool = (primaryKey != "") && (primarySignature != "") && (pExpected == pReceived)
    var validSecondary: Bool = (secondaryKey != "") && (secondarySignature != "") && (sExpected == sReceived)
    if (validPrimary == false) && (validSecondary == false) {
        print("Signature verification failed.")
        return
    }

    //  Parse JSON only after signature verification.
    let webhookEvent = CkoJsonObject()!
    success = webhookEvent.load(json: rawBody.getString(charset: "utf-8"))
    if success != true {
        print("\(webhookEvent.lastErrorText!)")
        return
    }

    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.

}