Webhook Security: Webhook Signatures July 2021 · ian

Introduction

When it comes to Webhooks, ensuring security is one of HostedHooks top priorities. This week we launched Webhook Signatures, which are a big step in making sure that our webhook solution has the most cutting edge security possible.

In this post we’ll walk through our Webhook Signature solution, show you how it works and provide a Ruby code snippet on how to use them (other languages are coming soon).

Webhook Security

Webhooks are really just messages that get sent across the open internet and with that comes some risk. In order to make sure no bad actors or third parties can interact with your webhooks we enlist a handful of security measures.

Security Measures

  • Enforce HTTPS (SSL) - We make sure that all subscriber endpoints are HTTPS (SSL) to ensure that all requests are encrypted and that no one in the middle can intercept the request.
  • Webhook Signatures - All Webhook messages are sent with a Signature (HTTP_HOSTEDHOOKS_SIGNATURE) in the webhook request header. This signature is used in conjunction with a Endpoint Secret to determine that the payload has not been tampered with and was sent by HostedHooks.
  • Timestamp - Along with the signature, we provide a timestamp (t=) with every webhook request. This timestamp is used to generate the Webhook Signature. To help protect against replay attacks, this timestamp should be used by the subscriber to make sure that the webhook was sent in an acceptable amount of time.

Webhook Signatures

Starting this week, every endpoint comes with a Webhook Signing Secret. This is a random hexadecimal string that is used to generate a Signature (HMAC) with the SHA256 hash function. The inputs for this function are the timestamp of when the message was sent and the payload of the message.

CleanShot 2021-07-05 at 11 21 21

HostedHooks will generate this signature internally and then add it as a header (HTTP_HOSTEDHOOKS_SIGNATURE) on every webhook message along with the timestamp.

The header will look like the below example, where s= is the payload signature and t= is the timestamp.

t=1623436092, s=7e526f3c14539d4d2856a1a2e8b1112c944cd466670041fe758fcc930d8cdf23

New lines have been added here to make it more readable, but the actual HTTP_HOSTEDHOOKS_SIGNATURE is on one line.

When a subscriber receives this webhook message via their endpoint, they can optionally choose to compare signatures to confirm that the payload has not been tampered with and that the message was sent within an acceptable amount of time.

We’ll walk through how to verify the signature using Ruby.

Webhooks Signature Verification

Using this Webhook Signature from the Header

t=1623436092, s=7e526f3c14539d4d2856a1a2e8b1112c944cd466670041fe758fcc930d8cdf23

Step 1: Parse the timestamp and signatures from the header

Split the header using the ',' to get both the 's' and 't' values. Then use the '=' to split the keys from the value. The 's' value corresponds to the payload signature and the 't' value corresponds to the timestamp.

Step 2: Create the signed_payload

To create the signed payload you will want to concatenate the following values

  • The timestamp
  • the character '.'
  • The JSON payload received (request body)

Step 3: Generate the expected Signature

Generate an HMAC with the SHA256 hash function. The key for the hash function is your endpoint's signing secret and the signed_payload string is the message we generated in Step 2.

Step 4: Compare the signatures

Compare the signature that you received in the header (Step 1) with the generated signature (Step 3). If those match, then calculate the difference between the current timestamp and the received timestamp (step 1) and determine if the difference is within an acceptable tolerance.

Webhook Signature Verification in Ruby

# Step 1: Grab the Signature from the Header and split out the values
# HTTP_HOSTEDHOOKS_SIGNATURE = "t=1623436092,s=7e526f3c14539d4d2856a1a2e8b1112c944cd466670041fe758fcc930d8cdf23"

hostedhooks_signature = request.headers['HTTP_HOSTEDHOOKS_SIGNATURE']
timestamp_string, signature_string = hosted_hooks_signature.split(',')

reference_timestamp = timestamp_string.split('=')[1]
reference_signature = signature_string.split('=')[1]

# Step 2: Concatenate the values for the signed payload 

endpoint.signing_secret = "f230b55338a95d7d5f4709dc80defe8caf5c7cab44dbf655"

# request.data = {"type":"user.created","version":"1.0","created":"2021-05-07T10:46:09.257-04:00","data":{"id":123123123,"note":"this is a test","other_id":1231231123}}
webhook_payload = request.raw_post
current_timestamp_in_seconds = DateTime.now.strftime('%s')

data = "#{current_timestamp_in_seconds}.#{webhook_payload.to_json}"

# Step 3: Generate the expected signature

digest = OpenSSL::Digest.new('sha256')
expected_signature = OpenSSL::HMAC.hexdigest(digest, key, data)

# Step 4: Compare the expected signature with reference signature. Then compare the current timestamp with the reference timestamp

if expected_signature == reference
  puts "Webhook signature is valid"
  puts "Webhook is too old" if (current_timestamp_in_seconds.to_i - reference_timestamp.to_i) > 5
else
  puts "Request is invalid"
end

We generate a new timestamp and signature when webhook messages are sent to the subscriber's endpoint. In the case of retries (where a previous webhook attempt returned a non 200 response), we will generate a new timestamp and signature.

Each endpoint will have it's own respective signing secret and cannot be shared between endpoints. If you need to rotate your signing secret, please reach out to support.

Conclusion

Using webhooks signatures are recommended as they will signficantly increase the security of your webhooks and data. We'll be releasing these signature verification snippets in other programming languages in the coming weeks.

You can find more documentation here - https://docs.hostedhooks.com/security/security-features/webhook-signatures

If you have any questions please reach out to support at hostedhooks.com

Ready to start sending Webhooks? Get started for Free