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

Lazarus Pascal
program ChilkatDemo;

// Demonstrates using the Chilkat Pascal wrapper via the C bridge DLL.
// Builds as a console application under Lazarus (FPC) or Delphi.

{$IFDEF FPC}
  {$MODE DELPHI}
{$ENDIF}
{$APPTYPE CONSOLE}

uses
  {$IFDEF UNIX}
  cthreads,
  {$ENDIF}
  SysUtils,
  CkDllLoader,
  Chilkat.JsonObject,
  Chilkat.BinData,
  Chilkat.Prng,
  Chilkat.CkDateTime,
  Chilkat.Crypt2;

// ---------------------------------------------------------------------------

procedure RunDemo;
var
  success: Boolean;
  rawBody: TBinData;
  timestamp: string;
  primarySignature: string;
  secondarySignature: string;
  signatureVersion: string;
  signatureAlgorithm: string;
  primaryKey: string;
  secondaryKey: string;
  delivered: TCkDateTime;
  now: TCkDateTime;
  age: Integer;
  signedBytes: TBinData;
  mac: TCrypt2;
  expectedPrimary: string;
  expectedSecondary: string;
  random: TPrng;
  comparisonKey: string;
  pExpected: string;
  pReceived: string;
  sExpected: string;
  sReceived: string;
  validPrimary: Boolean;
  validSecondary: Boolean;
  webhookEvent: TJsonObject;

begin
  success := 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.
  rawBody := TBinData.Create;
  success := rawBody.LoadFile('qa_data/box_webhook_body.bin');
  if (success <> True) then
    begin
      WriteLn(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
      WriteLn('Unsupported signature version or algorithm.');
      Exit;
    end;
  delivered := TCkDateTime.Create;
  success := delivered.SetFromTimestamp(timestamp);
  if (success <> True) then
    begin
      WriteLn(delivered.LastErrorText);
      Exit;
    end;
  now := TCkDateTime.Create;
  success := now.SetFromCurrentSystemTime();
  if (success <> True) then
    begin
      WriteLn(now.LastErrorText);
      Exit;
    end;
  age := StrToInt(now.GetAsUnixTime(False)) - StrToInt(delivered.GetAsUnixTime(False));
  //  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
      WriteLn('Delivery timestamp is outside the accepted window.');
      Exit;
    end;
  signedBytes := TBinData.Create;
  success := signedBytes.AppendBd(rawBody);
  if (success <> True) then
    begin
      WriteLn(signedBytes.LastErrorText);
      Exit;
    end;
  success := signedBytes.AppendString(timestamp,'utf-8');
  if (success <> True) then
    begin
      WriteLn(signedBytes.LastErrorText);
      Exit;
    end;
  mac := TCrypt2.Create;
  mac.MacAlgorithm := 'hmac';
  mac.HashAlgorithm := 'sha256';
  mac.EncodingMode := 'base64';
  success := mac.SetMacKeyString(primaryKey);
  if (success <> True) then
    begin
      WriteLn(mac.LastErrorText);
      Exit;
    end;
  expectedPrimary := mac.MacBdENC(signedBytes);
  if (mac.LastMethodSuccess <> True) then
    begin
      WriteLn(mac.LastErrorText);
      Exit;
    end;
  success := mac.SetMacKeyString(secondaryKey);
  if (success <> True) then
    begin
      WriteLn(mac.LastErrorText);
      Exit;
    end;
  expectedSecondary := mac.MacBdENC(signedBytes);
  if (mac.LastMethodSuccess <> True) then
    begin
      WriteLn(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 := TPrng.Create;
  comparisonKey := random.GenRandom(32,'base64');
  if (random.LastMethodSuccess <> True) then
    begin
      WriteLn(random.LastErrorText);
      Exit;
    end;
  success := mac.SetMacKeyEncoded(comparisonKey,'base64');
  if (success <> True) then
    begin
      WriteLn(mac.LastErrorText);
      Exit;
    end;
  pExpected := mac.MacStringENC(expectedPrimary);
  if (mac.LastMethodSuccess <> True) then
    begin
      WriteLn(mac.LastErrorText);
      Exit;
    end;
  pReceived := mac.MacStringENC(primarySignature);
  if (mac.LastMethodSuccess <> True) then
    begin
      WriteLn(mac.LastErrorText);
      Exit;
    end;
  sExpected := mac.MacStringENC(expectedSecondary);
  if (mac.LastMethodSuccess <> True) then
    begin
      WriteLn(mac.LastErrorText);
      Exit;
    end;
  sReceived := mac.MacStringENC(secondarySignature);
  if (mac.LastMethodSuccess <> True) then
    begin
      WriteLn(mac.LastErrorText);
      Exit;
    end;
  validPrimary := (primaryKey <> '') and (primarySignature <> '') and (pExpected = pReceived);
  validSecondary := (secondaryKey <> '') and (secondarySignature <> '') and (sExpected = sReceived);
  if ((validPrimary = False) and (validSecondary = False)) then
    begin
      WriteLn('Signature verification failed.');
      Exit;
    end;
  //  Parse JSON only after signature verification.
  webhookEvent := TJsonObject.Create;
  success := webhookEvent.Load(rawBody.GetString('utf-8'));
  if (success <> True) then
    begin
      WriteLn(webhookEvent.LastErrorText);
      Exit;
    end;
  WriteLn('Verified event ID: ' + webhookEvent.StringOf('id'));
  WriteLn('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.


  rawBody.Free;
  delivered.Free;
  now.Free;
  signedBytes.Free;
  mac.Free;
  random.Free;
  webhookEvent.Free;

end;

// ---------------------------------------------------------------------------

begin

  try
    RunDemo;
  except
    on E: Exception do
      WriteLn('Unhandled exception: ', E.ClassName, ': ', E.Message);
  end;

  WriteLn;
  {$IFDEF MSWINDOWS}
  WriteLn('Press Enter to exit...');
  ReadLn;
  {$ENDIF}
end.