server side support for cookies, basic tests
[disorder] / lib / cookies.c
1 /*
2 * This file is part of DisOrder
3 * Copyright (C) 2007 Richard Kettlewell
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful, but
11 * WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 * General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program; if not, write to the Free Software
17 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
18 * USA
19 */
20 /** @file lib/cookies.c
21 * @brief Cookie support
22 */
23
24 #include <config.h>
25 #include "types.h"
26
27 #include <stdlib.h>
28 #include <string.h>
29 #include <stdio.h>
30 #include <errno.h>
31 #include <time.h>
32 #include <gcrypt.h>
33
34 #include "cookies.h"
35 #include "hash.h"
36 #include "mem.h"
37 #include "log.h"
38 #include "printf.h"
39 #include "mime.h"
40 #include "configuration.h"
41 #include "kvp.h"
42
43 /** @brief Hash function used in signing HMAC */
44 #define ALGO GCRY_MD_SHA1
45
46 /** @brief Size of key to use */
47 #define HASHSIZE 20
48
49 /** @brief Signing key */
50 static uint8_t signing_key[HASHSIZE];
51
52 /** @brief Previous signing key */
53 static uint8_t old_signing_key[HASHSIZE];
54
55 /** @brief Signing key validity limit or 0 if none */
56 static time_t signing_key_validity_limit;
57
58 /** @brief Hash of revoked cookies */
59 static hash *revoked;
60
61 /** @brief Callback to expire revocation list */
62 static int revoked_cleanup_callback(const char *key, void *value,
63 void *u) {
64 if(*(time_t *)value < *(time_t *)u)
65 hash_remove(revoked, key);
66 return 0;
67 }
68
69 /** @brief Generate a new key */
70 static void newkey(void) {
71 time_t now;
72
73 time(&now);
74 memcpy(old_signing_key, signing_key, HASHSIZE);
75 gcry_randomize(signing_key, HASHSIZE, GCRY_STRONG_RANDOM);
76 signing_key_validity_limit = now + config->cookie_key_lifetime;
77 /* Now is a good time to clean up the revocation list... */
78 if(revoked)
79 hash_foreach(revoked, revoked_cleanup_callback, &now);
80 }
81
82 /** @brief Sign @p subject with @p key and return the base64 of the result
83 * @param key Key to sign with (@ref HASHSIZE bytes)
84 * @param subject Subject string
85 * @return Base64-encoded signature or NULL
86 */
87 static char *sign(const uint8_t *key,
88 const char *subject) {
89 gcry_error_t e;
90 gcry_md_hd_t h;
91 uint8_t *sig;
92 char *sig64;
93
94 if((e = gcry_md_open(&h, ALGO, GCRY_MD_FLAG_HMAC))) {
95 error(0, "gcry_md_open: %s", gcry_strerror(e));
96 return 0;
97 }
98 if((e = gcry_md_setkey(h, key, HASHSIZE))) {
99 error(0, "gcry_md_setkey: %s", gcry_strerror(e));
100 gcry_md_close(h);
101 return 0;
102 }
103 gcry_md_write(h, subject, strlen(subject));
104 sig = gcry_md_read(h, ALGO);
105 sig64 = mime_to_base64(sig, HASHSIZE);
106 gcry_md_close(h);
107 return sig64;
108 }
109
110 /** @brief Create a login cookie
111 * @param user Username
112 * @return Cookie or NULL
113 */
114 char *make_cookie(const char *user) {
115 char *password;
116 time_t now;
117 char *b, *bp, *c, *g;
118 int n;
119
120 /* semicolons aren't allowed in usernames */
121 if(strchr(user, ';')) {
122 error(0, "make_cookie for username with semicolon");
123 return 0;
124 }
125 /* look up the password */
126 for(n = 0; n < config->allow.n
127 && strcmp(config->allow.s[n].s[0], user); ++n)
128 ;
129 if(n >= config->allow.n) {
130 error(0, "make_cookie for nonexistent user");
131 return 0;
132 }
133 password = config->allow.s[n].s[1];
134 /* make sure we have a valid signing key */
135 time(&now);
136 if(now >= signing_key_validity_limit)
137 newkey();
138 /* construct the subject */
139 byte_xasprintf(&b, "%jx;%s;", (intmax_t)now + config->cookie_login_lifetime,
140 urlencodestring(user));
141 byte_xasprintf(&bp, "%s%s", b, password);
142 /* sign it */
143 if(!(g = sign(signing_key, bp)))
144 return 0;
145 /* put together the final cookie */
146 byte_xasprintf(&c, "%s%s", b, g);
147 return c;
148 }
149
150 /** @brief Verify a cookie
151 * @param cookie Cookie to verify
152 * @return Verified user or NULL
153 */
154 char *verify_cookie(const char *cookie) {
155 char *c1, *c2;
156 intmax_t t;
157 time_t now;
158 char *user, *bp, *password, *sig;
159 int n;
160
161 /* check the revocation list */
162 if(revoked && hash_find(revoked, cookie)) {
163 error(0, "attempt to log in with revoked cookie");
164 return 0;
165 }
166 /* parse the cookie */
167 errno = 0;
168 t = strtoimax(cookie, &c1, 16);
169 if(errno) {
170 error(errno, "error parsing cookie timestamp");
171 return 0;
172 }
173 if(*c1 != ';') {
174 error(0, "invalid cookie timestamp");
175 return 0;
176 }
177 /* There'd better be two semicolons */
178 c2 = strchr(c1 + 1, ';');
179 if(c2 == 0) {
180 error(0, "invalid cookie syntax");
181 return 0;
182 }
183 /* Extract the username */
184 user = xstrndup(c1 + 1, c2 - (c1 + 1));
185 /* check expiry */
186 time(&now);
187 if(now >= t) {
188 error(0, "cookie has expired");
189 return 0;
190 }
191 /* look up the password */
192 for(n = 0; n < config->allow.n
193 && strcmp(config->allow.s[n].s[0], user); ++n)
194 ;
195 if(n >= config->allow.n) {
196 error(0, "verify_cookie for nonexistent user");
197 return 0;
198 }
199 password = config->allow.s[n].s[1];
200 /* construct the expected subject. We re-encode the timestamp and the
201 * password. */
202 byte_xasprintf(&bp, "%jx;%s;%s", t, urlencodestring(user), password);
203 /* Compute the expected signature. NB we base64 the expected signature and
204 * compare that rather than exposing our base64 parser to the cookie. */
205 if(!(sig = sign(signing_key, bp)))
206 return 0;
207 if(!strcmp(sig, c2 + 1))
208 return user;
209 /* that didn't match, try the old key */
210 if(!(sig = sign(old_signing_key, bp)))
211 return 0;
212 if(!strcmp(sig, c2 + 1))
213 return user;
214 /* that didn't match either */
215 error(0, "cookie signature does not match");
216 return 0;
217 }
218
219 /** @brief Revoke a cookie
220 * @param cookie Cookie to revoke
221 *
222 * Further attempts to log in with @p cookie will fail.
223 */
224 void revoke_cookie(const char *cookie) {
225 time_t when;
226 char *ptr;
227
228 /* find the cookie's expiry time */
229 errno = 0;
230 when = (time_t)strtoimax(cookie, &ptr, 16);
231 /* reject bogus cookies */
232 if(errno)
233 return;
234 if(*ptr != ';')
235 return;
236 /* make sure the revocation list exists */
237 if(!revoked)
238 revoked = hash_new(sizeof(time_t));
239 /* add the cookie to it; its value is the expiry time */
240 hash_add(revoked, cookie, &when, HASH_INSERT);
241 }
242
243 /*
244 Local Variables:
245 c-basic-offset:2
246 comment-column:40
247 fill-column:79
248 indent-tabs-mode:nil
249 End:
250 */