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

Delphi ActiveX
var
success: Integer;
rawBody: TChilkatBinData;
timestamp: WideString;
primarySignature: WideString;
secondarySignature: WideString;
signatureVersion: WideString;
signatureAlgorithm: WideString;
primaryKey: WideString;
secondaryKey: WideString;
delivered: TCkDateTime;
now: TCkDateTime;
age: Integer;
signedBytes: TChilkatBinData;
mac: TChilkatCrypt2;
expectedPrimary: WideString;
expectedSecondary: WideString;
random: TChilkatPrng;
comparisonKey: WideString;
pExpected: WideString;
pReceived: WideString;
sExpected: WideString;
sReceived: WideString;
validPrimary: Integer;
validSecondary: Integer;
webhookEvent: TChilkatJsonObject;

begin
success := 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 := TChilkatBinData.Create(Self);
success := rawBody.LoadFile('qa_data/box_webhook_body.bin');
if (success <> 1) then
  begin
    Memo1.Lines.Add(rawBody.LastErrorText);
    Exit;
  end;
//  Read these fields case-insensitively from the incoming request headers.
//  For this offline example, enter the captured header values.
timestamp := 'BOX_DELIVERY_TIMESTAMP';
primarySignature := 'BOX_SIGNATURE_PRIMARY';
secondarySignature := 'BOX_SIGNATURE_SECONDARY';
signatureVersion := '1';
signatureAlgorithm := 'HmacSHA256';
//  Retrieve configured keys from protected application configuration.
primaryKey := 'BOX_PRIMARY_SIGNATURE_KEY';
secondaryKey := 'BOX_SECONDARY_SIGNATURE_KEY';
if ((signatureVersion <> '1') or (signatureAlgorithm <> 'HmacSHA256')) then
  begin
    Memo1.Lines.Add('Unsupported signature version or algorithm.');
    Exit;
  end;
delivered := TCkDateTime.Create(Self);
success := delivered.SetFromTimestamp(timestamp);
if (success <> 1) then
  begin
    Memo1.Lines.Add(delivered.LastErrorText);
    Exit;
  end;
now := TCkDateTime.Create(Self);
success := now.SetFromCurrentSystemTime();
if (success <> 1) then
  begin
    Memo1.Lines.Add(now.LastErrorText);
    Exit;
  end;
age := StrToInt(now.GetAsUnixTime(0)) - StrToInt(delivered.GetAsUnixTime(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)) then
  begin
    Memo1.Lines.Add('Delivery timestamp is outside the accepted window.');
    Exit;
  end;
signedBytes := TChilkatBinData.Create(Self);
success := signedBytes.AppendBd(rawBody.ControlInterface);
if (success <> 1) then
  begin
    Memo1.Lines.Add(signedBytes.LastErrorText);
    Exit;
  end;
success := signedBytes.AppendString(timestamp,'utf-8');
if (success <> 1) then
  begin
    Memo1.Lines.Add(signedBytes.LastErrorText);
    Exit;
  end;
mac := TChilkatCrypt2.Create(Self);
mac.MacAlgorithm := 'hmac';
mac.HashAlgorithm := 'sha256';
mac.EncodingMode := 'base64';
success := mac.SetMacKeyString(primaryKey);
if (success <> 1) then
  begin
    Memo1.Lines.Add(mac.LastErrorText);
    Exit;
  end;
expectedPrimary := mac.MacBdENC(signedBytes.ControlInterface);
if (mac.LastMethodSuccess <> 1) then
  begin
    Memo1.Lines.Add(mac.LastErrorText);
    Exit;
  end;
success := mac.SetMacKeyString(secondaryKey);
if (success <> 1) then
  begin
    Memo1.Lines.Add(mac.LastErrorText);
    Exit;
  end;
expectedSecondary := mac.MacBdENC(signedBytes.ControlInterface);
if (mac.LastMethodSuccess <> 1) then
  begin
    Memo1.Lines.Add(mac.LastErrorText);
    Exit;
  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.
random := TChilkatPrng.Create(Self);
comparisonKey := random.GenRandom(32,'base64');
if (random.LastMethodSuccess <> 1) then
  begin
    Memo1.Lines.Add(random.LastErrorText);
    Exit;
  end;
success := mac.SetMacKeyEncoded(comparisonKey,'base64');
if (success <> 1) then
  begin
    Memo1.Lines.Add(mac.LastErrorText);
    Exit;
  end;
pExpected := mac.MacStringENC(expectedPrimary);
if (mac.LastMethodSuccess <> 1) then
  begin
    Memo1.Lines.Add(mac.LastErrorText);
    Exit;
  end;
pReceived := mac.MacStringENC(primarySignature);
if (mac.LastMethodSuccess <> 1) then
  begin
    Memo1.Lines.Add(mac.LastErrorText);
    Exit;
  end;
sExpected := mac.MacStringENC(expectedSecondary);
if (mac.LastMethodSuccess <> 1) then
  begin
    Memo1.Lines.Add(mac.LastErrorText);
    Exit;
  end;
sReceived := mac.MacStringENC(secondarySignature);
if (mac.LastMethodSuccess <> 1) then
  begin
    Memo1.Lines.Add(mac.LastErrorText);
    Exit;
  end;
validPrimary := (primaryKey <> '') and (primarySignature <> '') and (pExpected = pReceived);
validSecondary := (secondaryKey <> '') and (secondarySignature <> '') and (sExpected = sReceived);
if ((validPrimary = 0) and (validSecondary = 0)) then
  begin
    Memo1.Lines.Add('Signature verification failed.');
    Exit;
  end;
//  Parse JSON only after signature verification.
webhookEvent := TChilkatJsonObject.Create(Self);
success := webhookEvent.Load(rawBody.GetString('utf-8'));
if (success <> 1) then
  begin
    Memo1.Lines.Add(webhookEvent.LastErrorText);
    Exit;
  end;
Memo1.Lines.Add('Verified event ID: ' + webhookEvent.StringOf('id'));
Memo1.Lines.Add('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.