Webhooks are HMAC authenticated!

At Logentries we provide alerting that allows users to get notified about important   events. Alerts are sent via email, notifications on iPhone, and via webhooks. Webhooks are ideal for situations where you want to kick off an automated response to an incident or event. Typical examples include application restarts, automatic scaling… and as you well know, the list goes on! Since webhooks, and thus your automated reactions to events, go about their business without a human being in the loop it makes sense to be extra careful here. Standard HTTP POST messages can be easily forged and that potentially opens a security hole when using webhooks. And that’s the reason why we have gone and implemented an authentication mechanism for all Webhooks sent via Logentries.

To enable the authentication, simply include a username and a password in Webhook’s URL. For example: http://user:password@example.com/webhook We use the Hash Message Authentication Code for this which provides a relatively simple mechanism to verify both authenticity and consistency of the message.  The idea is that for the request we calculate a cryptographic hash which is unfeasible to recreate without a shared secure code (that’s a password in our case). Both the sender and receiver calculates the hash value independently using their shared secure code and compares them for identity.

Note that HMAC is not a simple hash of the message content. Since standard cryptographic hashes are stream-based (Merkle–Damgård), they are prone to a length-extension attack. Thus, an attacker can easily pad the content to fit the hash block size and continue with the calculation of the hash with the appended data. To avoid this situation, HMAC uses a slightly more complex hashing scheme to close this (and a few other) holes. The following example explains what the HMAC looks like in HTTP headers. This is an example of an alert report configured to be sent to http://user:password@example.com/webhook. This means, authentication credentials (user, password) are encoded in the configuration URL.

POST /webhook HTTP/1.1
User-Agent: Logentries/1.2
Host: example.com
Date: Mon, 28 Jan 2013 22:01:58 GMT
Content-Type: application/x-www-form-urlencoded
Content-Md5: A4O7taYfMqO/3vugWHFriA==
Content-Length: 1632
Connection: keep-alive
X-Le-Nonce: nfblZ9aBldYSHT64Kw2bbVwt
X-Le-Account: f1cac763
Authorization: LE user:qc2s3YmnX42K1Nvtxw/p1Br1ehI=
Accept-Encoding: identity


The hash code is contained in the Authorization header encoded in base 64.  To calculate the hash, we create a canonical string which is hashed in a special manner to avoid some cryptographic attacks. The canonical string contains all important fields to guarantee that the message cannot be tempered without noticing. Since HTTP headers can be extended and/or modified on their way across proxies and load balancers, we have to select only a subset of them in order to get a stable result and still guarantee authenticity. These fields are method type (POST in this case), Content-Type, MD5 hash of the content (needs to be calculated although it’s duplicated in the header), Date, path, and X-Le-Nonce. Nonce (aka salt) is a random string generated on the server side to help with a detection of replay attacks. Firstly, we calculate MD5 of the POST data and encode it using base 64 encoding. The value of this hash is also stored in the HTTP header Content-Md5, but don’t use that for HMAC calculation as it can be tempered:

import hashlib, base64
content_md5 = base64.b64encode( hashlib.md5( content).digest())

The canonical string then contains all the selected headers delimited with new lines in the following ‘exact’ ordering. Assuming we store headers in variables, the code may look like this:

canonical = '\n'.join([ 'POST', content_type, content_md5, request_date, path, nonce ])

Hashing this canonical string produces a signature. Here is how to calculate it with a secret password using a hmac library:

import hmac
signature = base64.b64encode(
    hmac.new( password, canonical, hashlib.sha1).digest())

And the authentication header takes the following form, where username is a desired user’s name.

auth_header = 'LE ' +username +':' +signature

Note that comparing authentication headers is not enough however to be sure you are protected! We have to check that the Date is reasonably  accurate (say, not older than 30 seconds) and that nonce (unique for every webhook) hasn’t been seen yet to avoid replay attacks. Find all details (including Python and Ruby sample implementations) in docs. Consider the current implementation as in beta – we are actively testing it. If you have any feedback, let us know! We always love hearing from you and more importantly it makes Logentries stonger and stronger!

Viliam is a co-founder and in a position that could be called CTO.

Posted in Feature

Leave a Reply