Using Mohawk

There are two parties involved in Hawk communication: a sender and a receiver. They use a shared secret to sign and verify each other’s messages.

Sender
A client who wants to access a Hawk-protected resource. The client will sign their request and upon receiving a response will also verify the response signature.
Receiver
A server that uses Hawk to protect its resources. The server will check the signature of an incoming request before accepting it. It also signs its response using the same shared secret.

What are some good use cases for Hawk? This library was built for the case of securing API connections between two back-end servers. Hawk is a good fit for this because you can keep the shared secret safe on each machine. Hawk may not be a good fit for scenarios where you can’t protect the shared secret.

After getting familiar with usage, you may want to consult the Security Considerations section.

Sending a request

Let’s say you want to make an HTTP request like this:

>>> url = 'https://some-service.net/system'
>>> method = 'POST'
>>> content = 'one=1&two=2'
>>> content_type = 'application/x-www-form-urlencoded'

Set up your Hawk request by creating a mohawk.Sender object with all the elements of the request that you need to sign:

>>> from mohawk import Sender
>>> sender = Sender({'id': 'some-sender',
...                  'key': 'a long, complicated secret',
...                  'algorithm': 'sha256'},
...                 url,
...                 method,
...                 content=content,
...                 content_type=content_type)

This provides you with a Hawk Authorization header to send along with your request:

>>> sender.request_header
'Hawk mac="...", hash="...", id="some-sender", ts="...", nonce="..."'

Using the requests library just as an example, you would send your POST like this:

>>> requests.post(url, data=content,
...               headers={'Authorization': sender.request_header,
...                        'Content-Type': content_type})

Notice how both the content and content-type values were signed by the Sender. In the case of a GET request you’ll probably need to sign empty strings like Sender(..., 'GET', content='', content_type=''), that is, if your request library doesn’t automatically set a content-type for GET requests.

If you only intend to work with mohawk.Sender, skip down to Verifying a response.

Receiving a request

On the receiving end, such as a web server, you’ll need to set up a mohawk.Receiver object to accept and respond to mohawk.Sender requests.

First, you need to give the receiver a callable that it can use to look up sender credentials:

>>> def lookup_credentials(sender_id):
...     if sender_id in allowed_senders:
...         # Return a credentials dictionary formatted like the sender example.
...         return allowed_senders[sender_id]
...     else:
...         raise LookupError('unknown sender')

An incoming request will probably arrive in an object like this, depending on your web server framework:

>>> request = {'headers': {'Authorization': sender.request_header,
...                        'Content-Type': content_type},
...            'url': url,
...            'method': method,
...            'content': content}

Create a mohawk.Receiver using values from the incoming request:

>>> from mohawk import Receiver
>>> receiver = Receiver(lookup_credentials,
...                     request['headers']['Authorization'],
...                     request['url'],
...                     request['method'],
...                     content=request['content'],
...                     content_type=request['headers']['Content-Type'])

If this constructor does not raise any Exceptions then the signature of the request is correct and you can proceed.

Important

The server running mohawk.Receiver code should synchronize its clock with something like TLSdate to make sure it compares timestamps correctly.

Responding to a request

It’s optional per the Hawk spec but a mohawk.Receiver should sign its response back to the client to prevent certain attacks.

The receiver starts by building a message it wants to respond with:

>>> response_content = '{"msg": "Hello, dear friend"}'
>>> response_content_type = 'application/json'
>>> header = receiver.respond(content=response_content,
...                           content_type=response_content_type)

This provides you with a similar Hawk header to use in the response:

>>> receiver.response_header
'Hawk mac="...", hash="...="'

Using your web server’s framework, respond with a Server-Authorization header. For example:

>>> response = {
...     'headers': {'Server-Authorization': receiver.response_header,
...                 'Content-Type': response_content_type},
...     'content': response_content
... }

Verifying a response

When the mohawk.Sender receives a response it should verify the signature to make sure nothing has been tampered with:

>>> sender.accept_response(response['headers']['Server-Authorization'],
...                        content=response['content'],
...                        content_type=response['headers']['Content-Type'])

If this method does not raise any Exceptions then the signature of the response is correct and you can proceed.

Allowing senders to adjust their timestamps

The easiest way to avoid timestamp problems is to synchronize your server clock using something like TLSdate.

If a sender’s clock is out of sync with the receiver, its message might expire prematurely. In this case the receiver should respond with a header the sender can use to adjust its timestamp.

When receiving a request you might get a mohawk.exc.TokenExpired exception. You can access the www_authenticate property on the exception object to respond correctly like this:

>>> from mohawk.exc import TokenExpired
>>> try:
...     receiver = Receiver(lookup_credentials,
...                         request['headers']['Authorization'],
...                         request['url'],
...                         request['method'],
...                         content=request['content'],
...                         content_type=request['headers']['Content-Type'])
... except TokenExpired as expiry:
...     response['headers']['WWW-Authenticate'] = expiry.www_authenticate
...     print(expiry.www_authenticate)
Hawk ts="...", tsm="...", error="token with UTC timestamp...has expired..."

A compliant client can look for this response header and parse the ts property (the server’s “now” timestamp) and the tsm property (a MAC calculation of ts). It can then recalculate the MAC using its own credentials and if the MACs both match it can trust that this is the real server’s timestamp. This allows the sender to retry the request with an adjusted timestamp.

Using a nonce to prevent replay attacks

A replay attack is when someone copies a Hawk authorized message and re-sends the message without altering it. Because the Hawk signature would still be valid, the receiver may accept the message. This could have unintended side effects such as increasing the quantity of an item just purchased if it were a commerce API that had an increment-item service.

Hawk protects against replay attacks in a couple ways. First, a receiver checks the timestamp of the message which may result in a mohawk.exc.TokenExpired exception. Second, every message includes a cryptographic nonce which is a unique identifier. In combination with the sender’s id and the request’s timestamp, a receiver can use the nonce to know if it has already received the request. If so, the mohawk.exc.AlreadyProcessed exception is raised.

By default, Mohawk doesn’t know how to check nonce values; this is something your application needs to do.

Important

If you don’t configure nonce checking, your application could be susceptible to replay attacks.

Make a callable that returns True if a sender’s nonce plus its timestamp has been seen already. Here is an example using something like memcache:

>>> def seen_nonce(sender_id, nonce, timestamp):
...     key = '{id}:{nonce}:{ts}'.format(id=sender_id, nonce=nonce,
...                                      ts=timestamp)
...     if memcache.get(key):
...         # We have already processed this nonce + timestamp.
...         return True
...     else:
...         # Save this nonce + timestamp for later.
...         memcache.set(key, True)
...         return False

Because messages will expire after a short time you don’t need to store nonces for much longer than that timeout. See mohawk.Receiver for the default timeout.

Pass your callable as a seen_nonce argument to mohawk.Receiver:

>>> receiver = Receiver(lookup_credentials,
...                     request['headers']['Authorization'],
...                     request['url'],
...                     request['method'],
...                     content=request['content'],
...                     content_type=request['headers']['Content-Type'],
...                     seen_nonce=seen_nonce)

If seen_nonce() returns True, mohawk.exc.AlreadyProcessed will be raised.

When a sender calls mohawk.Sender.accept_response(), it will receive a Hawk message but the nonce will be that of the original request. In other words, the nonce received is the same nonce that the sender generated and signed when initiating the request. This generally means you don’t have to worry about response replay attacks. However, if you expose your mohawk.Sender.accept_response() call somewhere publicly over HTTP then you may need to protect against response replay attacks. You can do so by constructing a mohawk.Sender with the same seen_nonce keyword:

>>> sender = Sender({'id': 'some-sender',
...                  'key': 'a long, complicated secret',
...                  'algorithm': 'sha256'},
...                 url,
...                 method,
...                 content=content,
...                 content_type=content_type,
...                 seen_nonce=seen_nonce)

Skipping content checks

In some cases you may not be able to hash request/response content. For example, the content could be too large. If you run into this, Hawk might not be the best fit for you but Hawk does allow you to accept content without a declared hash if you wish.

Important

By allowing content without a declared hash, both the sender and receiver are susceptible to content tampering.

You can send a request without signing the content by passing this keyword argument to a mohawk.Sender:

>>> sender = Sender(credentials, url, method, always_hash_content=False)

This says to skip hashing of the content and content_type values if they are both mohawk.base.EmptyValue.

Now you’ll get an Authorization header without a hash attribute:

>>> sender.request_header
'Hawk mac="...", id="some-sender", ts="...", nonce="..."'

The mohawk.Receiver must also be constructed to accept content without a declared hash using accept_untrusted_content=True:

>>> receiver = Receiver(lookup_credentials,
...                     sender.request_header,
...                     request['url'],
...                     request['method'],
...                     content=request['content'],
...                     content_type=request['headers']['Content-Type'],
...                     accept_untrusted_content=True)

This will skip checking the hash of content and content_type only if the Authorization header omits the hash attribute. If the hash attribute is present, it will be checked as normal.

Empty requests

For requests whose content (and by extension content_type) is None or an empty string, it is acceptable for the sender to omit the declared hash, regardless of the accept_untrusted_content value provided to the mohawk.Receiver. For example, a GET request typically has empty content and some libraries may or may not hash the content.

If the hash attribute is present, a None value for either content or content_type will be coerced to an empty string prior to hashing.

Generating protected URLs

Hawk lets you protect a URL with a token derived from a secret key. After a period of time, access to the URL will expire. As an example, you could use this to deliver a URL for purchased media, such a zip file of MP3s. The user could access the URL for a short period of time but after that, the same URL would not be accessible.

In the Hawk spec, this is referred to as Single URI Authorization, or bewit.

Here’s an example of protecting access to this URL with Mohawk:

>>> url = 'https://site.org/purchases/music-album.zip'

Let’s say you want to allow access for 5 minutes:

>>> from mohawk.util import utc_now
>>> url_expires_at = utc_now() + (60 * 5)

Set up Hawk credentials like in previous examples:

>>> credentials = {
...     'id': 'some-recipient',
...     'key': 'a long, complicated secret',
...     'algorithm': 'sha256'
... }

Define the resource that you want to protect:

>>> from mohawk.base import Resource
>>> resource = Resource(
...     credentials=credentials,
...     url=url,
...     method='GET',
...     nonce='',
...     timestamp=url_expires_at,
... )

Generate a bewit token:

>>> from mohawk.bewit import get_bewit
>>> bewit = get_bewit(resource)

Add that token as a bewit query string parameter back to the same URL:

>>> protected_url = '{url}?bewit={bewit}'.format(url=url, bewit=bewit)
>>> protected_url
'https://site.org/purchases/music-album.zip?bewit=...'

Now you can deliver this bewit protected URL to the recipient.

Serving protected URLs

When handling a request for a bewit protected URL on the server, you can begin by checking the bewit to make sure it’s valid. If True, the server can respond with access to the resource. The check_bewit function returns True or False and will also raise an exception for invalid bewit values.

>>> allowed_recipients = {}
>>> allowed_recipients['some-recipient'] = credentials
>>> def lookup_credentials(recipient_id):
...     if recipient_id in allowed_recipients:
...         # Return a credentials dictionary
...         return allowed_recipients[recipient_id]
...     else:
...         raise LookupError('unknown recipient_id')
>>> from mohawk.bewit import check_bewit
>>> check_bewit(protected_url, credential_lookup=lookup_credentials)
True

Note

Well, that was complicated! At a future time, get_bewit and check_bewit will be complimented with a higher level function that is easier to work with. See https://github.com/kumar303/mohawk/issues/17

Logging

All internal logging channels stem from mohawk. For example, the mohawk.receiver channel will just contain receiver messages. These channels correspond to the submodules within mohawk.

To debug mohawk.exc.MacMismatch Exceptions and other authorization errors, set the mohawk channel to DEBUG.

Going further

Well, hey, that about summarizes the concepts and basic usage of Mohawk. Check out the API for details. Also make sure you are familiar with Security Considerations.