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

Objective-C
#import <CkoBinData.h>
#import <NSString.h>
#import <CkoDateTime.h>
#import <CkoCrypt2.h>
#import <CkoPrng.h>
#import <CkoJsonObject.h>

BOOL success = NO;

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

success = NO;

//  Save the exact HTTP body bytes to this file, without JSON reserialization.
CkoBinData *rawBody = [[CkoBinData alloc] init];
success = [rawBody LoadFile: @"qa_data/box_webhook_body.bin"];
if (success != YES) {
    NSLog(@"%@",rawBody.LastErrorText);
    return;
}

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

CkoDateTime *delivered = [[CkoDateTime alloc] init];
success = [delivered SetFromTimestamp: timestamp];
if (success != YES) {
    NSLog(@"%@",delivered.LastErrorText);
    return;
}

CkoDateTime *now = [[CkoDateTime alloc] init];
success = [now SetFromCurrentSystemTime];
if (success != YES) {
    NSLog(@"%@",now.LastErrorText);
    return;
}

int age = [[now GetAsUnixTime: NO] intValue] - [[delivered GetAsUnixTime: NO] 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)) {
    NSLog(@"%@",@"Delivery timestamp is outside the accepted window.");
    return;
}

CkoBinData *signedBytes = [[CkoBinData alloc] init];
success = [signedBytes AppendBd: rawBody];
if (success != YES) {
    NSLog(@"%@",signedBytes.LastErrorText);
    return;
}

success = [signedBytes AppendString: timestamp charset: @"utf-8"];
if (success != YES) {
    NSLog(@"%@",signedBytes.LastErrorText);
    return;
}

CkoCrypt2 *mac = [[CkoCrypt2 alloc] init];
mac.MacAlgorithm = @"hmac";
mac.HashAlgorithm = @"sha256";
mac.EncodingMode = @"base64";
success = [mac SetMacKeyString: primaryKey];
if (success != YES) {
    NSLog(@"%@",mac.LastErrorText);
    return;
}

NSString *expectedPrimary = [mac MacBdENC: signedBytes];
if (mac.LastMethodSuccess != YES) {
    NSLog(@"%@",mac.LastErrorText);
    return;
}

success = [mac SetMacKeyString: secondaryKey];
if (success != YES) {
    NSLog(@"%@",mac.LastErrorText);
    return;
}

NSString *expectedSecondary = [mac MacBdENC: signedBytes];
if (mac.LastMethodSuccess != YES) {
    NSLog(@"%@",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.
CkoPrng *random = [[CkoPrng alloc] init];
NSString *comparisonKey = [random GenRandom: [NSNumber numberWithInt: 32] encoding: @"base64"];
if (random.LastMethodSuccess != YES) {
    NSLog(@"%@",random.LastErrorText);
    return;
}

success = [mac SetMacKeyEncoded: comparisonKey encoding: @"base64"];
if (success != YES) {
    NSLog(@"%@",mac.LastErrorText);
    return;
}

NSString *pExpected = [mac MacStringENC: expectedPrimary];
if (mac.LastMethodSuccess != YES) {
    NSLog(@"%@",mac.LastErrorText);
    return;
}

NSString *pReceived = [mac MacStringENC: primarySignature];
if (mac.LastMethodSuccess != YES) {
    NSLog(@"%@",mac.LastErrorText);
    return;
}

NSString *sExpected = [mac MacStringENC: expectedSecondary];
if (mac.LastMethodSuccess != YES) {
    NSLog(@"%@",mac.LastErrorText);
    return;
}

NSString *sReceived = [mac MacStringENC: secondarySignature];
if (mac.LastMethodSuccess != YES) {
    NSLog(@"%@",mac.LastErrorText);
    return;
}

BOOL validPrimary = (primaryKey != @"") && (primarySignature != @"") && (pExpected == pReceived);
BOOL validSecondary = (secondaryKey != @"") && (secondarySignature != @"") && (sExpected == sReceived);
if ((validPrimary == NO) && (validSecondary == NO)) {
    NSLog(@"%@",@"Signature verification failed.");
    return;
}

//  Parse JSON only after signature verification.
CkoJsonObject *webhookEvent = [[CkoJsonObject alloc] init];
success = [webhookEvent Load: [rawBody GetString: @"utf-8"]];
if (success != YES) {
    NSLog(@"%@",webhookEvent.LastErrorText);
    return;
}

NSLog(@"%@%@",@"Verified event ID: ",[webhookEvent StringOf: @"id"]);
NSLog(@"%@%@",@"Trigger: ",[webhookEvent StringOf: @"trigger"]);
//  Deduplicate using event.id, not BOX-DELIVERY-ID, which changes on retries.
//  Verify the target belongs to your configured webhook before dispatching work.