Skip to content

Commit

Permalink
Provide 'sign' and 'verify' to secure cookies with HMAC
Browse files Browse the repository at this point in the history
The two functions are very basic and provide a layer of security for
cookies.

 - sign cookies with HMAC given a cookie, checksum type and secret key
 - verify a previously signed cookie using the same checksum type and
   secret key

The computed signature is:

    HMAC(checksum_type, key, HMAC(checksum_type, key, value) + name) + value

It guarantees that we have produced the value and associated it with a
name, which are the only data sent back by the user agent.

It's up to the developer to decide what hash algorithm to use and how to
handle its secret key.

The reverse procedure firstly check the length of a signature using
checksum_type on an arbitrary string so that it can extract the checksum
from the value without the need of any separator.

Add tests to handle specific cases:

 - signing an empty cookie
 - signing and verifying
 - verifying a cookie
 - verifying a too small signature

Add documentation to cover 'sign' and 'verify' processes.

fixup
  • Loading branch information
arteymix committed Aug 16, 2015
1 parent c590429 commit ea889b4
Show file tree
Hide file tree
Showing 4 changed files with 160 additions and 0 deletions.
42 changes: 42 additions & 0 deletions docs/vsgi/cookies.rst
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,45 @@ The newly created cookie can be sent by adding a ``Set-Cookie`` header in the
var cookie = new Cookie ("name", "value", "0.0.0.0", "/", 60);
res.headers.append ("Set-Cookie", cookie.to_set_cookie_header ());
Sign and verify
---------------

Considering that cookies are persisted by the user agent, it might be necessary
to sign to prevent forgery. ``Cookies.sign`` and ``Cookies.verify`` functions
are provided for the purposes of signing and verifying cookies.

.. warning::

Be careful when you choose and store the secret key. Also, changing it will
break any previously signed cookies, which may still be submitted by user
agents.

It's up to you to choose what hashing algorithm and secret: ``SHA512`` is
generally recommended.

The signature process is the following:

::

HMAC (checksum_type, key, HMAC (checksum_type, key, value) + name) + value

It guarantees that:

- we have produced the value
- we have produced the name and associated it to the value

The verification process does not handle special cases like values smaller than
the hashing: cookies are either signed or not, even if their values are
incorrectly formed.

.. code:: vala
var @value = Cookies.sign (cookie, ChecksumType.SHA512, "secret".data);
cookie.@value = @value;
string @value;
if (Cookies.verify (cookie, ChecksumType.SHA512, "secret.data", out @value)) {
// cookie's okay and the original value is stored in @value
}
51 changes: 51 additions & 0 deletions src/vsgi/cookies.vala
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,55 @@ namespace VSGI.Cookies {

return found;
}

/**
* Sign the provided cookie name and value using HMAC.
*
* The returned value will be 'HMAC(checksum_type, name + HMAC(checksum_type, value)) + value'
* suitable for a cookie value which can the be verified with {@link VSGI.Cookies.verify}.
*
* {{
* cookie.@value = Cookies.sign (cookie, ChecksumType.SHA512, "super-secret".data);
* }}
*
* @param cookie cookie to sign
* @param checksum_type hash algorithm used to compute the HMAC
* @param key secret used to sign the cookie name and value
* @return the signed value for the provided cookie, which can
* be reassigned in the cookie
*/
public string sign (Cookie cookie, ChecksumType checksum_type, uint8[] key) {
var checksum = Hmac.compute_for_string (checksum_type,
key,
Hmac.compute_for_string (checksum_type, key, cookie.@value) + cookie.name);

return checksum + cookie.@value;
}

/**
* Verify a signed cookie from {@link VSGI.Cookies.sign}.
*
* @param cookie
* @param checksum_type hash algorithm used to compute the HMAC
* @param key secret used to sign the cookie's value
* @param value cookie's value extracted from its signature if the
* verification succeeds, null otherwise
* @return true if the cookie is signed by the secret
*/
public bool verify (Cookie cookie, ChecksumType checksum_type, uint8[] key, out string? @value = null) {
var checksum_length = Hmac.compute_for_string (checksum_type, key, "").length;

if (cookie.@value.length < checksum_length)
return false;

@value = cookie.@value.substring (checksum_length);

var checksum = Hmac.compute_for_string (checksum_type,
key,
Hmac.compute_for_string (checksum_type, key, @value) + cookie.name);

assert (checksum_length == checksum.length);

return cookie.@value.substring (0, checksum_length) == checksum;
}
}
5 changes: 5 additions & 0 deletions tests/tests.vala
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,11 @@ public int main (string[] args) {
Test.add_func ("/vsgi/cookies/from_request", test_vsgi_cookies_from_request);
Test.add_func ("/vsgi/cookies/from_response", test_vsgi_cookies_from_response);
Test.add_func ("/vsgi/cookies/lookup", test_vsgi_cookies_lookup);
Test.add_func ("/vsgi/cookies/sign", test_vsgi_cookies_sign);
Test.add_func ("/vsgi/cookies/sign/empty_cookie", test_vsgi_cookies_sign_empty_cookie);
Test.add_func ("/vsgi/cookies/sign_and_verify", test_vsgi_cookies_sign_and_verify);
Test.add_func ("/vsgi/cookies/verify", test_vsgi_cookies_verify);
Test.add_func ("/vsgi/cookies/verify/too_small_value", test_vsgi_cookies_verify_too_small_value);

Test.add_func ("/vsgi/chunked_encoder", test_vsgi_chunked_encoder);

Expand Down
62 changes: 62 additions & 0 deletions tests/vsgi/test_cookies.vala
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,65 @@ public void test_vsgi_cookies_lookup () {
assert ("a" == cookie.name);
assert ("c" == cookie.value);
}

/**
* @since 0.2
*/
public void test_vsgi_cookies_sign () {
var cookie = new Soup.Cookie ("name", "value", "0.0.0.0", "/", 3600);

var signature = VSGI.Cookies.sign (cookie, ChecksumType.SHA256, "secret".data);

assert ("1f45ecc8f4d5a281d0f1ed0085a448a9814d960dd0aa0e8fc933820e0f4d7ff93d5259bc8d3d0887eade2221c515e208954714defedffbf195895245f10b30a4value" == signature);
}

/**
* @since 0.2
*/
public void test_vsgi_cookies_sign_empty_cookie () {
var cookie = new Soup.Cookie ("name", "", "0.0.0.0", "/", 3600);
var signature = VSGI.Cookies.sign (cookie, ChecksumType.SHA256, "secret".data);

assert ("e28b8f776996beb02e1d45d2ce4603013f3fcbb7353fc2d4fa5999be1b1c164652a4675387a41a44e17b283441e47889f5b6f539c0ab0704ce789ebff4e52377" == signature);
}

/**
* @since 0.2
*/
public void test_vsgi_cookies_sign_and_verify () {
var cookie = new Soup.Cookie ("name", "value", "0.0.0.0", "/", 3600);

cookie.set_value (VSGI.Cookies.sign (cookie, ChecksumType.SHA256, "secret".data));

string @value;
assert (VSGI.Cookies.verify (cookie, ChecksumType.SHA256, "secret".data, out @value));
assert ("value" == @value);
}

/**
* @since 0.2
*/
public void test_vsgi_cookies_verify () {
var cookie = new Soup.Cookie ("name",
"1f45ecc8f4d5a281d0f1ed0085a448a9814d960dd0aa0e8fc933820e0f4d7ff93d5259bc8d3d0887eade2221c515e208954714defedffbf195895245f10b30a4value",
"0.0.0.0",
"/",
3600);

string @value;
assert (VSGI.Cookies.verify (cookie, ChecksumType.SHA256, "secret".data, out @value));
assert ("value" == @value);
}

/**
* @since 0.2
*/
public void test_vsgi_cookies_verify_too_small_value () {
var cookie = new Soup.Cookie ("name", "value", "0.0.0.0", "/", 3600);

assert ("value".length < Hmac.compute_for_string (ChecksumType.SHA256, "secret".data, "value").length);

string @value;
assert (!VSGI.Cookies.verify (cookie, ChecksumType.SHA256, "secret".data, out @value));
assert (null == @value);
}

0 comments on commit ea889b4

Please sign in to comment.