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.
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
Try HostedHooks Free
Getting started is easy! No credit card required.