Verkada can optionally sign the webhook events it sends to your endpoints by including a signature in each event’s Verkada-Signature header. This allows you to verify that the events were sent by Verkada, not by a third party.

Before you can verify signatures, you need to retrieve your shared secret that was configured in Organization settings by Organization Admin.

Preventing replay attacks

A replay attack is when an attacker intercepts a valid payload and its signature, then re-transmits them. To mitigate such attacks, Verkada includes a timestamp in the Verkada-Signature header. Because this timestamp is part of the signed payload, it is also verified by the signature, so an attacker cannot change the timestamp without invalidating the signature. If the signature is valid but the timestamp is too old, you can have your application reject the payload.

Verkada generates the timestamp and signature each time an event is sent to your endpoint. If Verkada retries an event (e.g., your endpoint previously replied with a non-2xx status code), then a new signature and timestamp are generated for the new delivery attempt.

Verkada-Signature:1597685622|b41406dd45b7f8b7f7e564442c23b4d8615e711890dc7a02cfb3e445bd98afd5

The Verkada-Signature header included in each signed event contains a timestamp and a signature separated by |. The first token of the string is a timestamp and the second one is the signature.

Verifying Signature

Step 1: Extract the timestamp and signatures from the header
Split the header, using the | character as the separator, to get a list of tokens. The first token of the string is a timestamp and the second one is the signature.

Step 2: Prepare the signed_payload string

The signed_payload string is created by concatenating:

The actual JSON payload (i.e., the request body)
The character |
The timestamp (as a string)

Step 3: Determine the expected signature
Compute an HMAC with the SHA256 hash function. Use the webhook shared secret as the key, and use the signed_payload string as the message.

Step 4: Compare the signatures
Compare the signature (or signatures) in the header to the expected signature. For an equality match, compute the difference between the current timestamp and the received timestamp, then decide if the difference is within your tolerance.

To protect against timing attacks, use constant-time string comparison to compare the expected signature to each of the received signatures.

try:
  timestamp, signature = request.headers['Verkada-Signature'].split('|')
except KeyError:
  raise ApiError(message='Missing signature', code=400)
        
# Check if request was sent in last minute
if time.time() - int(timestamp) > 60:
  raise ApiError(message='Expired', code=403)

to_hash = b'|'.join((request.get_data() or b'', timestamp.encode()))
if not compare_digest(signature.encode(), hmac.new(
  shared_secret, msg=to_hash, digestmod=hashlib.sha256
).hexdigest().encode()):
   raise ApiError(message='Invalid signature', code=403)