Signature Verification

Signature Verification

Webhook signatures help ensure that a webhook payload was sent by Box and was not tampered with. Signatures greatly reduce the likelihood of a successful man-in-the-middle or replay attacks.

When signatures are configured, Box generates a cryptographic digest of the notification's body and attaches it to the header of the webhook payload. When your application receives the payload, verify the signatures by calculating the same digest and comparing it to the one received. If the digests do not match, the payload should not be trusted.

You can achieve an extra level of protection by frequently changing the signature keys. To enable a smooth transition between the old and new keys, Box supports two simultaneous signature keys.

Signature configuration

In order to attach signatures to an application's notifications, you must first generate signature keys for the application.

To configure your application's keys follow the steps below.

  1. Navigate to the application in the developer console.
  2. Click on the Webhooks tab.
  3. Click the Manage signature keys button.
  4. Click the Generate Key button to configure your keys.

Once generating the primary or secondary key, copy the value. You will need it to verify the webhook payloads. Every webhook will now include a BOX-SIGNATURE-PRIMARY and BOX-SIGNATURE-SECONDARY header payload.

Signature verification with SDKs

Although it is possible to verify signatures manually, methods are provided for your convenience in the official Box SDKs.

Java
// Webhook message contents are shown for demonstration purposes
// Normally these would come from your HTTP handler

// Webhook message HTTP body
String messagePayload = "{"
    + "\"type\":\"webhook_event","
    + "\"webhook\":{"
    +   "\"id\":\"1234567890\""
    + "},"
    + "\"trigger\":\"FILE.UPLOADED\","
    + "\"source\":{"
    +   "\"id\":\"1234567890\","
    +   "\"type\":\"file\","
    +   "\"name\":\"Test.txt\""
    + "}}";

// Webhook message HTTP headers
Map<String, String> messageHeaders = new HashMap<String, String>();
headers.put("BOX-DELIVERY-ID", "f96bb54b-ee16-4fc5-aa65-8c2d9e5b546f");
headers.put("BOX-DELIVERY-TIMESTAMP", "2020-01-01T00:00:00-07:00");
headers.put("BOX-SIGNATURE-ALGORITHM", "HmacSHA256");
headers.put("BOX-SIGNATURE-PRIMARY", "6TfeAW3A1PASkgboxxA5yqHNKOwFyMWuEXny/FPD5hI=");
headers.put("BOX-SIGNATURE-SECONDARY", "v+1CD1Jdo3muIcbpv5lxxgPglOqMfsNHPV899xWYydo=");
headers.put("BOX-SIGNATURE-VERSION", "1");

// Your application's webhook keys, obtained from the Box Developer Console
String primaryKey = "4py2I9eSFb0ezXH5iPeQRcFK1LRLCdip";
String secondaryKey = "Aq5EEEjAu4ssbz8n9UMu7EerI0LKj2TL";

BoxWebHookSignatureVerifier verifier = new BoxWebHookSignatureVerifier(primaryKey, secondaryKey);
boolean isValidMessage = verifier.verify(
    headers.get("BOX-SIGNATURE-VERSION"),
    headers.get("BOX-SIGNATURE-ALGORITHM"),
    headers.get("BOX-SIGNATURE-PRIMARY"),
    headers.get("BOX-SIGNATURE-SECONDARY"),
    messagePayload,
    headers.get("BOX-DELIVERY-TIMESTAMP")
);

if (isValidMessage) {
    // Message is valid, handle it
} else {
    // Message is invalid, reject it
}
Python
body = b'{"webhook":{"id":"1234567890"},"trigger":"FILE.UPLOADED","source":{"id":"1234567890","type":"file","name":"Test.txt"}}'
headers = {
    'box-delivery-id': 'f96bb54b-ee16-4fc5-aa65-8c2d9e5b546f',
    'box-delivery-timestamp': '2020-01-01T00:00:00-07:00',
    'box-signature-algorithm': 'HmacSHA256',
    'box-signature-primary': '4KvFa5/unRL8aaqOlnbInTwkOmieZkn1ZVzsAJuRipE=',
    'box-signature-secondary': 'yxxwBNk7tFyQSy95/VNKAf1o+j8WMPJuo/KcFc7OS0Q=',
    'box-signature-version': '1',
}
is_validated = Webhook.validate_message(body, headers, primary_key, secondary_key)
print(f'The webhook message is validated to: {is_validated}')
.NET
using Box.V2.Managers;

var body = "{\"type\":\"webhook_event\",\"webhook\":{\"id\":\"1234567890\"},\"trigger\":\"FILE.UPLOADED\",\"source\":{\"id\":\"1234567890\",\"type\":\"file\",\"name\":\"Test.txt\"}}";
var headers = new Dictionary<string, string>()
{
    { "box-delivery-id", "f96bb54b-ee16-4fc5-aa65-8c2d9e5b546f" },
    { "box-delivery-timestamp", "2020-01-01T00:00:00-07:00" },
    { "box-signature-algorithm", "HmacSHA256" } ,
    { "box-signature-primary", "6TfeAW3A1PASkgboxxA5yqHNKOwFyMWuEXny/FPD5hI=" },
    { "box-signature-secondary", "v+1CD1Jdo3muIcbpv5lxxgPglOqMfsNHPV899xWYydo=" },
    { "box-signature-version", "1" }
};
var primaryKey = "Fd28OJrZ8oNxkgmS7TbjXNgrG8v";
var secondaryKey = "KWkROAOiof4zhYUHbAmiVn63cMj"

bool isValid = BoxWebhooksManager.VerifyWebhook(
    deliveryTimestamp: headers["box-delivery-timestamp"],
    signaturePrimary: headers["box-signature-primary"],
    signatureSecondary: headers["box-signature-secondary"],
    payload: body,
    primaryWebhookKey: primaryKey,
    secondaryWebhookKey: secondaryKey
);
Node
const BoxSDK = require('box-node-sdk');
let body = '{"type":"webhook_event","webhook":{"id":"1234567890"},"trigger":"FILE.UPLOADED","source":{"id":"1234567890","type":"file","name":"Test.txt"}}',
	headers = {
		'box-delivery-id': 'f96bb54b-ee16-4fc5-aa65-8c2d9e5b546f',
		'box-delivery-timestamp': '2020-01-01T00:00:00-07:00',
		'box-signature-algorithm': 'HmacSHA256',
		'box-signature-primary': '6TfeAW3A1PASkgboxxA5yqHNKOwFyMWuEXny/FPD5hI=',
		'box-signature-secondary': 'v+1CD1Jdo3muIcbpv5lxxgPglOqMfsNHPV899xWYydo=',
		'box-signature-version': '1'
	},
	primaryKey = 'SamplePrimaryKey',
	secondaryKey = 'SampleSecondaryKey';

let isValid = BoxSDK.validateWebhookMessage(body, headers, primaryKey, secondaryKey);
if (isValid) {
	// message is valid, accept
} else {
	// message is NOT valid, reject
}

Manual signature verification

The following steps describe the basics of how to verify a signature.

Timestamp validation

Check if the timestamp in the BOX-DELIVERY-TIMESTAMP header of the payload is not older than ten minutes.

Node
var timestamp = headers['BOX-DELIVERY-TIMESTAMP'];
var date = Date.parse(timestamp);
var expired = Date.now() - date > 10*60*1000;
Python
import dateutil.parser
import pytz
import datetime

timestamp = headers["BOX-DELIVERY-TIMESTAMP"]
date = dateutil.parser.parse(timestamp).astimezone(pytz.utc)

now = datetime.datetime.now(pytz.utc)
delta = datetime.timedelta(minutes=10)
expiry_date = now - deltaMinutes

expired = date >= expiry_date

Calculate HMAC signature

Calculate the HMAC of the payload using either one of the two configured signatures for the application in the Developer Console.

Ensure you append the bytes of the payload body first, and then the bytes of the timestamp found in the BOX-DELIVERY-TIMESTAMP header.

Node
var crypto = require('crypto');

var primaryKey = '...';
var secondaryKey = '...';

var payload = '{"type":"webhook_event"...}';

var hmac1 = crypto.createHmac('sha256', primaryKey);
hmac1.update(payload);
hmac1.update(timestamp);

var hmac2 = crypto.createHmac('sha256', secondaryKey);
hmac2.update(payload);
hmac2.update(timestamp);
Python
import hmac
import hashlib

primary_key = '...'
secondary_key = '...'

payload = "{\"type\":\"webhook_event\"...}"

bytes = bytes(payload, 'utf-8') + bytes(timestamp, 'utf-8')

hmac1 = hmac.new(primary_key, bytes, hashlib.sha256).digest()
hmac2 = hmac.new(secondary_key, bytes, hashlib.sha256).digest()

Base64 Conversion

Convert the HMAC to a Base64 encoded digest.

Node
var digest1 = hmac1.digest('base64');
var digest2 = hmac2.digest('base64');
Python
import base64

digest1 = base64.b64encode(hmac1)
digest2 = base64.b64encode(hmac2)

Signature comparison

Compare the encoded digest with the value of the BOX-SIGNATURE-PRIMARY or BOX-SIGNATURE-SECONDARY headers.

Compare the value of the BOX-SIGNATURE-PRIMARY header to the digest created with the primary key, and the value of the BOX-SIGNATURE-SECONDARY header to the digest created with the secondary key.

Node
var signature1 = headers['BOX-SIGNATURE-SECONDARY'];
var signature2 = headers['BOX-SIGNATURE-PRIMARY'];

var primarySignatureValid = digest1 === signature1
var secondarySignatureValid = digest2 === signature2

var valid = !expired && (primarySignatureValid || secondarySignatureValid)
Python
signature1 = headers["BOX-SIGNATURE-SECONDARY"]
signature2 = headers["BOX-SIGNATURE-PRIMARY"]

primary_sig_valid = digest1 === signature1
secondary_sig_valid = digest2 === signature2

valid = !expired && (primary_sig_valid || secondary_sig_valid)

HTTP header names are case insensitive. Your client should convert all header names to a standardized lowercase or uppercase format before trying to determine the value of a header.

Rotate signatures

When enabled, Box sends two signatures with every webhook payload. Your application can trust a payload as long as at least one of its signatures is valid. When updating one signature key at a time your application will always receive a payload with at least one valid signature.

Rotation steps

These instructions assume that you have already created a primary and secondary key in the Developer Console and you are ready to replace either of them.

By following these steps you can configure your application with two new keys without any conflicts.

  1. Go to the Webhooks tab in the Developer Console.
  2. Click the Manage signatures keys.
  3. Click the Reset button to change the primary key.
  4. Update your application with the new primary key. Your application can still receive notifications with the old primary key, but your webhooks should be processed correctly since the secondary key is still valid.
  5. Once you are confident that no webhooks with the old primary key are in-flight, you can update the secondary key using the same process.