From cf46a12292cf15303d68d27c6ba4155ecc2b4586 Mon Sep 17 00:00:00 2001 From: Ian Wilson Date: Sun, 3 Jun 2012 13:01:15 -0700 Subject: Expose signed_serialize and signed_deserialize in UnencryptedCookieSessionFactoryConfig. --- pyramid/session.py | 106 +++++++++++++++++++++++------------------- pyramid/tests/test_session.py | 42 +++++++++++++++++ 2 files changed, 100 insertions(+), 48 deletions(-) diff --git a/pyramid/session.py b/pyramid/session.py index 76b2b30b1..40e21ddbc 100644 --- a/pyramid/session.py +++ b/pyramid/session.py @@ -33,6 +33,53 @@ def manage_accessed(wrapped): accessed.__doc__ = wrapped.__doc__ return accessed +def signed_serialize(data, secret): + """ Serialize any pickleable structure (``data``) and sign it + using the ``secret`` (must be a string). Return the + serialization, which includes the signature as its first 40 bytes. + The ``signed_deserialize`` method will deserialize such a value. + + This function is useful for creating signed cookies. For example: + + .. code-block:: python + + cookieval = signed_serialize({'a':1}, 'secret') + response.set_cookie('signed_cookie', cookieval) + """ + pickled = pickle.dumps(data, pickle.HIGHEST_PROTOCOL) + sig = hmac.new(bytes_(secret), pickled, sha1).hexdigest() + return sig + native_(base64.b64encode(pickled)) + +def signed_deserialize(serialized, secret, hmac=hmac): + """ Deserialize the value returned from ``signed_serialize``. If + the value cannot be deserialized for any reason, a + :exc:`ValueError` exception will be raised. + + This function is useful for deserializing a signed cookie value + created by ``signed_serialize``. For example: + + .. code-block:: python + + cookieval = request.cookies['signed_cookie'] + data = signed_deserialize(cookieval, 'secret') + """ + # hmac parameterized only for unit tests + try: + input_sig, pickled = (serialized[:40], + base64.b64decode(bytes_(serialized[40:]))) + except (binascii.Error, TypeError) as e: + # Badly formed data can make base64 die + raise ValueError('Badly formed base64 data: %s' % e) + + sig = hmac.new(bytes_(secret), pickled, sha1).hexdigest() + + # Avoid timing attacks (see + # http://seb.dbzteam.org/crypto/python-oauth-timing-hmac.pdf) + if strings_differ(sig, input_sig): + raise ValueError('Invalid signature') + + return pickle.loads(pickled) + def UnencryptedCookieSessionFactoryConfig( secret, timeout=1200, @@ -43,6 +90,8 @@ def UnencryptedCookieSessionFactoryConfig( cookie_secure=False, cookie_httponly=False, cookie_on_exception=True, + signed_serialize=signed_serialize, + signed_deserialize=signed_deserialize, ): """ Configure a :term:`session factory` which will provide unencrypted @@ -89,6 +138,15 @@ def UnencryptedCookieSessionFactoryConfig( If ``True``, set a session cookie even if an exception occurs while rendering a view. Default: ``True``. + ``signed_serialize`` + A callable which takes more or less arbitrary python data structure and + a secret and returns a signed serialization in bytes. + Default: ``signed_serialize`` (using pickle). + + ``signed_deserialize`` + A callable which takes a signed and serialized data structure in bytes + and a secret and returns the original data structure if the signature + is valid. Default: ``signed_deserialize`` (using pickle). """ @implementer(ISession) @@ -225,51 +283,3 @@ def UnencryptedCookieSessionFactoryConfig( return True return UnencryptedCookieSessionFactory - -def signed_serialize(data, secret): - """ Serialize any pickleable structure (``data``) and sign it - using the ``secret`` (must be a string). Return the - serialization, which includes the signature as its first 40 bytes. - The ``signed_deserialize`` method will deserialize such a value. - - This function is useful for creating signed cookies. For example: - - .. code-block:: python - - cookieval = signed_serialize({'a':1}, 'secret') - response.set_cookie('signed_cookie', cookieval) - """ - pickled = pickle.dumps(data, pickle.HIGHEST_PROTOCOL) - sig = hmac.new(bytes_(secret), pickled, sha1).hexdigest() - return sig + native_(base64.b64encode(pickled)) - -def signed_deserialize(serialized, secret, hmac=hmac): - """ Deserialize the value returned from ``signed_serialize``. If - the value cannot be deserialized for any reason, a - :exc:`ValueError` exception will be raised. - - This function is useful for deserializing a signed cookie value - created by ``signed_serialize``. For example: - - .. code-block:: python - - cookieval = request.cookies['signed_cookie'] - data = signed_deserialize(cookieval, 'secret') - """ - # hmac parameterized only for unit tests - try: - input_sig, pickled = (serialized[:40], - base64.b64decode(bytes_(serialized[40:]))) - except (binascii.Error, TypeError) as e: - # Badly formed data can make base64 die - raise ValueError('Badly formed base64 data: %s' % e) - - sig = hmac.new(bytes_(secret), pickled, sha1).hexdigest() - - # Avoid timing attacks (see - # http://seb.dbzteam.org/crypto/python-oauth-timing-hmac.pdf) - if strings_differ(sig, input_sig): - raise ValueError('Invalid signature') - - return pickle.loads(pickled) - diff --git a/pyramid/tests/test_session.py b/pyramid/tests/test_session.py index 6d75c7950..5143b7a95 100644 --- a/pyramid/tests/test_session.py +++ b/pyramid/tests/test_session.py @@ -205,6 +205,48 @@ class TestUnencryptedCookieSession(unittest.TestCase): self.assertTrue(token) self.assertTrue('_csrft_' in session) + def test_serialize_option(self): + from pyramid.response import Response + secret = 'secret' + request = testing.DummyRequest() + session = self._makeOne(request, + signed_serialize=dummy_signed_serialize) + session['key'] = 'value' + response = Response() + self.assertEqual(session._set_cookie(response), True) + cookie = response.headerlist[-1][1] + expected_cookieval = dummy_signed_serialize( + (session.accessed, session.created, {'key': 'value'}), secret) + response = Response() + response.set_cookie('session', expected_cookieval) + expected_cookie = response.headerlist[-1][1] + self.assertEqual(cookie, expected_cookie) + + def test_deserialize_option(self): + import time + secret = 'secret' + request = testing.DummyRequest() + accessed = time.time() + state = {'key': 'value'} + cookieval = dummy_signed_serialize((accessed, accessed, state), secret) + request.cookies['session'] = cookieval + session = self._makeOne(request, + signed_deserialize=dummy_signed_deserialize) + self.assertEqual(dict(session), state) + +def dummy_signed_serialize(data, secret): + import base64 + from pyramid.compat import pickle, bytes_ + pickled = pickle.dumps(data) + return base64.b64encode(bytes_(secret)) + base64.b64encode(pickled) + +def dummy_signed_deserialize(serialized, secret): + import base64 + from pyramid.compat import pickle, bytes_ + serialized_data = base64.b64decode( + serialized[len(base64.b64encode(bytes_(secret))):]) + return pickle.loads(serialized_data) + class Test_manage_accessed(unittest.TestCase): def _makeOne(self, wrapped): from pyramid.session import manage_accessed -- cgit v1.2.3 From 2cc0710ecf9b47e5a6f41caf6b1c56b29bab2db0 Mon Sep 17 00:00:00 2001 From: Ian Joseph Wilson Date: Sun, 17 Jun 2012 10:40:51 -0700 Subject: Sign contributors agreement. --- CONTRIBUTORS.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index c3995aaba..5ccac50d3 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -176,3 +176,5 @@ Contributors - Marc Abramowitz, 2012/06/13 - Jeff Cook, 2012/06/16 + +- Ian Wilson, 2012/06/17 -- cgit v1.2.3