Objective-C Requires Chilkat v11.5.0+
Objective-C
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
#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.