Security

Verifying webhooks were sent by CAPTUR3D

Verifying signatures via Svix

CAPTUR3D uses a 3rd party service called Svix to facilitate the sending of webhooks. Svix has a number of official libraries which make it trivial to verify the authenticity of your incoming webhooks.

Using one of the Svix libraries is the easiest way to verify webhooks. Alternatively you can follow the instructions below to manually verify your webhooks.

Verifying signatures manually

Each webhook call includes three HTTP headers with information that is used for verification purposes.

  • svix-id - the unique message identifier for the webhook message. This identifier is unique across all messages, but will be the same when the same webhook is being resent (e.g. due to a previous failure).

  • svix-timestamp - timestamp in seconds since epoch.

  • svix-signature - the Base64 encoded list of signatures (space delimited)

Generating the expected Signature

Elements required to generate the expected signature:
  • The svix-id and svix-timestamp headers,

  • The raw request body from the incoming webhook

  • The signing secret (Base64 decoded with prefix removed) for your endpoint

Using these elements we can ensure that the webhook is coming from CAPTUR3D and has not been altered.

Process

The process for generating the expected signature is as follows:

  1. [signed payload] - Concatenate the svix-id header, svix-timestamp header, and request body with a . (period) between them.

  2. [secret] - Remove the whsec_ prefix from your endpoint’s signing secret and Base64 decode it.

  3. [raw signature] - Generate a HMAC with a SHA-256 digest using the secret as the key, and the signed payload as the data

  4. [signature] - Base64 encode the raw signature

Example (in ruby)
# from request
svix_id = 'msg_loFOjxBNrRLzqYUf'
svix_timestamp = '1731705121'
svix_signature = 'v1,rAvfW3dJ/X/qxhsaXPOyyCGmRKsaKWcsNccKXlIktD0='
payload = '{"event_type":"ping","data":{"success":true}}'

# from endpoint details
endpoint_secret = 'whsec_plJ3nmyCDGBKInavdOK15jsl'

# GENERATING SIGNATURE
# step 1
signed_content = "#{svix_id}.#{svix_timestamp}.#{payload}"

# step 2
secret_without_prefix = endpoint_secret.sub('whsec_', '')
secret = Base64.decode64(secret_without_prefix)

# step 3
raw_signature = OpenSSL::HMAC.digest("sha256", secret, signed_content)

# step 4
signature = Base64.strict_encode64(raw_signature)
# => "rAvfW3dJ/X/qxhsaXPOyyCGmRKsaKWcsNccKXlIktD0="

# compare with signature from header (minus the "v1," from the start)
request_signature = svix_signature.sub('v1,', '')

signature == request_signature 
# => true

The svix-signature header is composed of a list of space delimited signatures and their corresponding version identifiers. The signature list is most commonly of length one, though there could be any number of signatures.

e.g. v1,signature v2,signature2

Make sure to remove the version prefix and delimiter (e.g. v1,) before comparing the signature.

Replay Attacks

A replay attack is when an attacker intercepts a valid payload (including the signature), and re-transmits it to your endpoint. This payload will pass signature validation, and will therefore be acted upon.

To mitigate this attack, Svix includes a timestamp for when the webhook attempt occurred. Svix’s libraries automatically reject webhooks with a timestamp that are more than five minutes away (past or future) from the current time.

We recommend if you are manually verifying the signature, that you also compare the timestamp from the headers with the current time to ensure that it’s within a suitable tolerance. Svix uses a tolerance of 5 minutes. Anything outside of this will be rejected.

# from request headers
svix_timestamp = '1731705121'

# compare integers
header_timestamp = svix_timestamp.to_i # => 1731705121
current_time = Time.now.to_i # => 1739332257

TOLERANCE = 5 * 60 # 5 minutes

if (current_time - header_timestamp).abs > TOLERANCE
  puts "Timestamp is too far in the past or future"
else
  puts "Timestamp is within tolerance"
end

# => "Timestamp is too far in the past or future"