Skip to content

Commit

Permalink
Change default values of content, content_type for hash checks (#45)
Browse files Browse the repository at this point in the history
  • Loading branch information
rneilson authored and kumar303 committed Apr 17, 2017
1 parent b2f1bc0 commit 561802e
Show file tree
Hide file tree
Showing 7 changed files with 148 additions and 64 deletions.
10 changes: 10 additions & 0 deletions docs/index.rst
Expand Up @@ -113,6 +113,16 @@ Changelog
- **Breaking**: Escape characters in header values are no longer allowed,
potentially breaking clients that depend on this behavior.
- Introduced max limit of 4096 characters in the Authorization header
- Changed default values of ``content`` and ``content_type`` arguments to
:class:`mohawk.base.EmptyValue` in order to differentiate between
misconfiguration and cases where these arguments are explicitly given as
``None`` (as with some web frameworks). See :ref:`skipping-content-checks`
for more details.
- Failing to pass ``content`` and ``content_type`` arguments to
:class:`mohawk.Receiver` or :method:`mohawk.Sender.accept_response`
without specifying ``accept_untrusted_content=True`` will now raise
:class:`mohawk.exc.MissingContent` instead of :exception:`ValueError`.


- **0.3.4** (2017-01-07)

Expand Down
22 changes: 14 additions & 8 deletions docs/usage.rst
Expand Up @@ -351,15 +351,15 @@ the same ``seen_nonce`` keyword:
Skipping content checks
=======================

In some cases you may not be able to sign request/response content. For example,
the content could be too large to fit in memory. If you run into this, Hawk
might not be the best fit for you but Hawk does allow you to skip content
checks if you wish.
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 skipping content checks both the sender and receiver are
susceptible to content tampering.
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 :class:`mohawk.Sender`:
Expand All @@ -378,17 +378,23 @@ Now you'll get an ``Authorization`` header without a ``hash`` attribute:
>>> sender.request_header
u'Hawk mac="...", id="some-sender", ts="...", nonce="..."'

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

.. doctest:: usage

>>> 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.

Logging
=======

Expand Down
33 changes: 27 additions & 6 deletions mohawk/base.py
Expand Up @@ -8,7 +8,8 @@
from .exc import (AlreadyProcessed,
MacMismatch,
MisComputedContentHash,
TokenExpired)
TokenExpired,
MissingContent)
from .util import (calculate_mac,
calculate_payload_hash,
calculate_ts_mac,
Expand All @@ -21,6 +22,26 @@
log = logging.getLogger(__name__)


class HawkEmptyValue(object):

def __eq__(self, other):
return isinstance(other, self.__class__)

def __ne__(self, other):
return (not self.__eq__(other))

def __nonzero__(self):
return False

def __bool__(self):
return False

def __repr__(self):
return 'EmptyValue'

EmptyValue = HawkEmptyValue()


class HawkAuthority:

def _authorize(self, mac_type, parsed_header, resource,
Expand Down Expand Up @@ -154,8 +175,8 @@ def __init__(self, **kw):
self.credentials = kw.pop('credentials')
self.credentials['id'] = prepare_header_val(self.credentials['id'])
self.method = kw.pop('method').upper()
self.content = kw.pop('content', None)
self.content_type = kw.pop('content_type', None)
self.content = kw.pop('content', EmptyValue)
self.content_type = kw.pop('content_type', EmptyValue)
self.always_hash_content = kw.pop('always_hash_content', True)
self.ext = kw.pop('ext', None)
self.app = kw.pop('app', None)
Expand Down Expand Up @@ -193,14 +214,14 @@ def content_hash(self):
return self._content_hash

def gen_content_hash(self):
if self.content is None or self.content_type is None:
if self.content == EmptyValue or self.content_type == EmptyValue:
if self.always_hash_content:
# Be really strict about allowing developers to skip content
# hashing. If they get this far they may be unintentiionally
# skipping it.
raise ValueError(
raise MissingContent(
'payload content and/or content_type cannot be '
'empty without an explicit allowance')
'empty when always_hash_content is True')
log.debug('NOT hashing content')
self._content_hash = None
else:
Expand Down
8 changes: 8 additions & 0 deletions mohawk/exc.py
Expand Up @@ -101,3 +101,11 @@ class InvalidBewit(HawkFail):
The bewit is invalid; e.g. it doesn't contain the right number of
parameters.
"""


class MissingContent(HawkFail):
"""
A payload's `content` or `content_type` were not provided.
See :ref:`skipping-content-checks` for details.
"""
37 changes: 19 additions & 18 deletions mohawk/receiver.py
@@ -1,7 +1,10 @@
import logging
import sys

from .base import default_ts_skew_in_seconds, HawkAuthority, Resource
from .base import (default_ts_skew_in_seconds,
HawkAuthority,
Resource,
EmptyValue)
from .exc import CredentialsLookupError, MissingAuthorization
from .util import (calculate_mac,
parse_authorization_header,
Expand Down Expand Up @@ -33,17 +36,15 @@ class Receiver(HawkAuthority):
:param method: Method of the request. E.G. POST, GET
:type method: str
:param content=None: Byte string of request body.
:type content=None: str
:param content=EmptyValue: Byte string of request body.
:type content=EmptyValue: str
:param content_type=None: content-type header value for request.
:type content_type=None: str
:param content_type=EmptyValue: content-type header value for request.
:type content_type=EmptyValue: str
:param accept_untrusted_content=False:
When True, allow requests that do not hash their content or
allow None type ``content`` and ``content_type``
arguments. Read :ref:`skipping-content-checks`
to learn more.
When True, allow requests that do not hash their content.
Read :ref:`skipping-content-checks` to learn more.
:type accept_untrusted_content=False: bool
:param localtime_offset_in_seconds=0:
Expand All @@ -65,8 +66,8 @@ def __init__(self,
request_header,
url,
method,
content=None,
content_type=None,
content=EmptyValue,
content_type=EmptyValue,
seen_nonce=None,
localtime_offset_in_seconds=0,
accept_untrusted_content=False,
Expand Down Expand Up @@ -120,8 +121,8 @@ def __init__(self,
self.resource = resource

def respond(self,
content=None,
content_type=None,
content=EmptyValue,
content_type=EmptyValue,
always_hash_content=True,
ext=None):
"""
Expand All @@ -130,14 +131,14 @@ def respond(self,
This generates the :attr:`mohawk.Receiver.response_header`
attribute.
:param content=None: Byte string of response body that will be sent.
:type content=None: str
:param content=EmptyValue: Byte string of response body that will be sent.
:type content=EmptyValue: str
:param content_type=None: content-type header value for response.
:type content_type=None: str
:param content_type=EmptyValue: content-type header value for response.
:type content_type=EmptyValue: str
:param always_hash_content=True:
When True, ``content`` and ``content_type`` cannot be None.
When True, ``content`` and ``content_type`` must be provided.
Read :ref:`skipping-content-checks` to learn more.
:type always_hash_content=True: bool
Expand Down
37 changes: 19 additions & 18 deletions mohawk/sender.py
@@ -1,6 +1,9 @@
import logging

from .base import default_ts_skew_in_seconds, HawkAuthority, Resource
from .base import (default_ts_skew_in_seconds,
HawkAuthority,
Resource,
EmptyValue)
from .util import (calculate_mac,
parse_authorization_header,
validate_credentials)
Expand All @@ -23,14 +26,14 @@ class Sender(HawkAuthority):
:param method: Method of the request. E.G. POST, GET
:type method: str
:param content=None: Byte string of request body.
:type content=None: str
:param content=EmptyValue: Byte string of request body.
:type content=EmptyValue: str
:param content_type=None: content-type header value for request.
:type content_type=None: str
:param content_type=EmptyValue: content-type header value for request.
:type content_type=EmptyValue: str
:param always_hash_content=True:
When True, ``content`` and ``content_type`` cannot be None.
When True, ``content`` and ``content_type`` must be provided.
Read :ref:`skipping-content-checks` to learn more.
:type always_hash_content=True: bool
Expand Down Expand Up @@ -68,8 +71,8 @@ class Sender(HawkAuthority):
def __init__(self, credentials,
url,
method,
content=None,
content_type=None,
content=EmptyValue,
content_type=EmptyValue,
always_hash_content=True,
nonce=None,
ext=None,
Expand Down Expand Up @@ -102,8 +105,8 @@ def __init__(self, credentials,

def accept_response(self,
response_header,
content=None,
content_type=None,
content=EmptyValue,
content_type=EmptyValue,
accept_untrusted_content=False,
localtime_offset_in_seconds=0,
timestamp_skew_in_seconds=default_ts_skew_in_seconds,
Expand All @@ -116,18 +119,16 @@ def accept_response(self,
such as one created by :class:`mohawk.Receiver`.
:type response_header: str
:param content=None: Byte string of the response body received.
:type content=None: str
:param content=EmptyValue: Byte string of the response body received.
:type content=EmptyValue: str
:param content_type=None:
:param content_type=EmptyValue:
Content-Type header value of the response received.
:type content_type=None: str
:type content_type=EmptyValue: str
:param accept_untrusted_content=False:
When True, allow responses that do not hash their content or
allow None type ``content`` and ``content_type``
arguments. Read :ref:`skipping-content-checks`
to learn more.
When True, allow responses that do not hash their content.
Read :ref:`skipping-content-checks` to learn more.
:type accept_untrusted_content=False: bool
:param localtime_offset_in_seconds=0:
Expand Down

0 comments on commit 561802e

Please sign in to comment.