Skip to content

Commit ea889b4

Browse files
committed
Provide 'sign' and 'verify' to secure cookies with HMAC
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
1 parent c590429 commit ea889b4

File tree

4 files changed

+160
-0
lines changed

4 files changed

+160
-0
lines changed

docs/vsgi/cookies.rst

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,45 @@ The newly created cookie can be sent by adding a ``Set-Cookie`` header in the
8686
8787
var cookie = new Cookie ("name", "value", "0.0.0.0", "/", 60);
8888
res.headers.append ("Set-Cookie", cookie.to_set_cookie_header ());
89+
90+
Sign and verify
91+
---------------
92+
93+
Considering that cookies are persisted by the user agent, it might be necessary
94+
to sign to prevent forgery. ``Cookies.sign`` and ``Cookies.verify`` functions
95+
are provided for the purposes of signing and verifying cookies.
96+
97+
.. warning::
98+
99+
Be careful when you choose and store the secret key. Also, changing it will
100+
break any previously signed cookies, which may still be submitted by user
101+
agents.
102+
103+
It's up to you to choose what hashing algorithm and secret: ``SHA512`` is
104+
generally recommended.
105+
106+
The signature process is the following:
107+
108+
::
109+
110+
HMAC (checksum_type, key, HMAC (checksum_type, key, value) + name) + value
111+
112+
It guarantees that:
113+
114+
- we have produced the value
115+
- we have produced the name and associated it to the value
116+
117+
The verification process does not handle special cases like values smaller than
118+
the hashing: cookies are either signed or not, even if their values are
119+
incorrectly formed.
120+
121+
.. code:: vala
122+
123+
var @value = Cookies.sign (cookie, ChecksumType.SHA512, "secret".data);
124+
125+
cookie.@value = @value;
126+
127+
string @value;
128+
if (Cookies.verify (cookie, ChecksumType.SHA512, "secret.data", out @value)) {
129+
// cookie's okay and the original value is stored in @value
130+
}

src/vsgi/cookies.vala

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,4 +68,55 @@ namespace VSGI.Cookies {
6868

6969
return found;
7070
}
71+
72+
/**
73+
* Sign the provided cookie name and value using HMAC.
74+
*
75+
* The returned value will be 'HMAC(checksum_type, name + HMAC(checksum_type, value)) + value'
76+
* suitable for a cookie value which can the be verified with {@link VSGI.Cookies.verify}.
77+
*
78+
* {{
79+
* cookie.@value = Cookies.sign (cookie, ChecksumType.SHA512, "super-secret".data);
80+
* }}
81+
*
82+
* @param cookie cookie to sign
83+
* @param checksum_type hash algorithm used to compute the HMAC
84+
* @param key secret used to sign the cookie name and value
85+
* @return the signed value for the provided cookie, which can
86+
* be reassigned in the cookie
87+
*/
88+
public string sign (Cookie cookie, ChecksumType checksum_type, uint8[] key) {
89+
var checksum = Hmac.compute_for_string (checksum_type,
90+
key,
91+
Hmac.compute_for_string (checksum_type, key, cookie.@value) + cookie.name);
92+
93+
return checksum + cookie.@value;
94+
}
95+
96+
/**
97+
* Verify a signed cookie from {@link VSGI.Cookies.sign}.
98+
*
99+
* @param cookie
100+
* @param checksum_type hash algorithm used to compute the HMAC
101+
* @param key secret used to sign the cookie's value
102+
* @param value cookie's value extracted from its signature if the
103+
* verification succeeds, null otherwise
104+
* @return true if the cookie is signed by the secret
105+
*/
106+
public bool verify (Cookie cookie, ChecksumType checksum_type, uint8[] key, out string? @value = null) {
107+
var checksum_length = Hmac.compute_for_string (checksum_type, key, "").length;
108+
109+
if (cookie.@value.length < checksum_length)
110+
return false;
111+
112+
@value = cookie.@value.substring (checksum_length);
113+
114+
var checksum = Hmac.compute_for_string (checksum_type,
115+
key,
116+
Hmac.compute_for_string (checksum_type, key, @value) + cookie.name);
117+
118+
assert (checksum_length == checksum.length);
119+
120+
return cookie.@value.substring (0, checksum_length) == checksum;
121+
}
71122
}

tests/tests.vala

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,11 @@ public int main (string[] args) {
112112
Test.add_func ("/vsgi/cookies/from_request", test_vsgi_cookies_from_request);
113113
Test.add_func ("/vsgi/cookies/from_response", test_vsgi_cookies_from_response);
114114
Test.add_func ("/vsgi/cookies/lookup", test_vsgi_cookies_lookup);
115+
Test.add_func ("/vsgi/cookies/sign", test_vsgi_cookies_sign);
116+
Test.add_func ("/vsgi/cookies/sign/empty_cookie", test_vsgi_cookies_sign_empty_cookie);
117+
Test.add_func ("/vsgi/cookies/sign_and_verify", test_vsgi_cookies_sign_and_verify);
118+
Test.add_func ("/vsgi/cookies/verify", test_vsgi_cookies_verify);
119+
Test.add_func ("/vsgi/cookies/verify/too_small_value", test_vsgi_cookies_verify_too_small_value);
115120

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

tests/vsgi/test_cookies.vala

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,65 @@ public void test_vsgi_cookies_lookup () {
6565
assert ("a" == cookie.name);
6666
assert ("c" == cookie.value);
6767
}
68+
69+
/**
70+
* @since 0.2
71+
*/
72+
public void test_vsgi_cookies_sign () {
73+
var cookie = new Soup.Cookie ("name", "value", "0.0.0.0", "/", 3600);
74+
75+
var signature = VSGI.Cookies.sign (cookie, ChecksumType.SHA256, "secret".data);
76+
77+
assert ("1f45ecc8f4d5a281d0f1ed0085a448a9814d960dd0aa0e8fc933820e0f4d7ff93d5259bc8d3d0887eade2221c515e208954714defedffbf195895245f10b30a4value" == signature);
78+
}
79+
80+
/**
81+
* @since 0.2
82+
*/
83+
public void test_vsgi_cookies_sign_empty_cookie () {
84+
var cookie = new Soup.Cookie ("name", "", "0.0.0.0", "/", 3600);
85+
var signature = VSGI.Cookies.sign (cookie, ChecksumType.SHA256, "secret".data);
86+
87+
assert ("e28b8f776996beb02e1d45d2ce4603013f3fcbb7353fc2d4fa5999be1b1c164652a4675387a41a44e17b283441e47889f5b6f539c0ab0704ce789ebff4e52377" == signature);
88+
}
89+
90+
/**
91+
* @since 0.2
92+
*/
93+
public void test_vsgi_cookies_sign_and_verify () {
94+
var cookie = new Soup.Cookie ("name", "value", "0.0.0.0", "/", 3600);
95+
96+
cookie.set_value (VSGI.Cookies.sign (cookie, ChecksumType.SHA256, "secret".data));
97+
98+
string @value;
99+
assert (VSGI.Cookies.verify (cookie, ChecksumType.SHA256, "secret".data, out @value));
100+
assert ("value" == @value);
101+
}
102+
103+
/**
104+
* @since 0.2
105+
*/
106+
public void test_vsgi_cookies_verify () {
107+
var cookie = new Soup.Cookie ("name",
108+
"1f45ecc8f4d5a281d0f1ed0085a448a9814d960dd0aa0e8fc933820e0f4d7ff93d5259bc8d3d0887eade2221c515e208954714defedffbf195895245f10b30a4value",
109+
"0.0.0.0",
110+
"/",
111+
3600);
112+
113+
string @value;
114+
assert (VSGI.Cookies.verify (cookie, ChecksumType.SHA256, "secret".data, out @value));
115+
assert ("value" == @value);
116+
}
117+
118+
/**
119+
* @since 0.2
120+
*/
121+
public void test_vsgi_cookies_verify_too_small_value () {
122+
var cookie = new Soup.Cookie ("name", "value", "0.0.0.0", "/", 3600);
123+
124+
assert ("value".length < Hmac.compute_for_string (ChecksumType.SHA256, "secret".data, "value").length);
125+
126+
string @value;
127+
assert (!VSGI.Cookies.verify (cookie, ChecksumType.SHA256, "secret".data, out @value));
128+
assert (null == @value);
129+
}

0 commit comments

Comments
 (0)