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.