1 /* ====================================================================
2 * The Apache Software License, Version 1.1
3 *
4 * Copyright (c) 2000-2003 The Apache Software Foundation. All rights
5 * reserved.
6 *
7 * Redistribution and use in source and binary forms, with or without
8 * modification, are permitted provided that the following conditions
9 * are met:
10 *
11 * 1. Redistributions of source code must retain the above copyright
12 * notice, this list of conditions and the following disclaimer.
13 *
14 * 2. Redistributions in binary form must reproduce the above copyright
15 * notice, this list of conditions and the following disclaimer in
16 * the documentation and/or other materials provided with the
17 * distribution.
18 *
19 * 3. The end-user documentation included with the redistribution,
20 * if any, must include the following acknowledgment:
21 * "This product includes software developed by the
22 * Apache Software Foundation (http://www.apache.org/)."
23 * Alternately, this acknowledgment may appear in the software itself,
24 * if and wherever such third-party acknowledgments normally appear.
25 *
26 * 4. The names "Apache" and "Apache Software Foundation" must
27 * not be used to endorse or promote products derived from this
28 * software without prior written permission. For written
29 * permission, please contact apache@apache.org.
30 *
31 * 5. Products derived from this software may not be called "Apache",
32 * nor may "Apache" appear in their name, without prior written
33 * permission of the Apache Software Foundation.
34 *
35 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
36 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
37 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
38 * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
39 * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
40 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
41 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
42 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
43 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
44 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
45 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
46 * SUCH DAMAGE.
47 * ====================================================================
48 *
49 * This software consists of voluntary contributions made by many
50 * individuals on behalf of the Apache Software Foundation. For more
51 * information on the Apache Software Foundation, please see
52 * <http://www.apache.org/>.
53 *
54 * Portions of this software are based upon public domain software
55 * originally written at the National Center for Supercomputing Applications,
56 * University of Illinois, Urbana-Champaign.
57 */
58
59 /*
60 * mod_digest: MD5 digest authentication
61 *
62 * by Alexei Kosut <akosut@nueva.pvt.k12.ca.us>
63 * based on mod_auth, by Rob McCool and Robert S. Thau
64 *
65 */
66
67 #include "httpd.h"
68 #include "http_config.h"
69 #include "http_core.h"
70 #include "http_log.h"
71 #include "http_protocol.h"
72 #include "util_md5.h"
73
74 typedef struct digest_config_struct {
75 char *pwfile;
76 } digest_config_rec;
77
78 typedef struct digest_header_struct {
79 char *username;
80 char *realm;
81 char *nonce;
82 char *requested_uri;
83 char *digest;
84 } digest_header_rec;
85
create_digest_dir_config(pool * p,char * d)86 static void *create_digest_dir_config(pool *p, char *d)
87 {
88 return ap_pcalloc(p, sizeof(digest_config_rec));
89 }
90
set_digest_slot(cmd_parms * cmd,void * offset,char * f,char * t)91 static const char *set_digest_slot(cmd_parms *cmd, void *offset, char *f, char *t)
92 {
93 if (t && strcmp(t, "standard"))
94 return ap_pstrcat(cmd->pool, "Invalid auth file type: ", t, NULL);
95
96 return ap_set_string_slot(cmd, offset, f);
97 }
98
99 static const command_rec digest_cmds[] =
100 {
101 {"AuthDigestFile", set_digest_slot,
102 (void *) XtOffsetOf(digest_config_rec, pwfile), OR_AUTHCFG, TAKE12, NULL},
103 {NULL}
104 };
105
106 module MODULE_VAR_EXPORT digest_module;
107
get_hash(request_rec * r,char * user,char * auth_pwfile)108 static char *get_hash(request_rec *r, char *user, char *auth_pwfile)
109 {
110 configfile_t *f;
111 char l[MAX_STRING_LEN];
112 const char *rpw;
113 char *w, *x;
114
115 if (!(f = ap_pcfg_openfile(r->pool, auth_pwfile))) {
116 ap_log_rerror(APLOG_MARK, APLOG_ERR, r,
117 "Could not open password file: %s", auth_pwfile);
118 return NULL;
119 }
120 while (!(ap_cfg_getline(l, MAX_STRING_LEN, f))) {
121 if ((l[0] == '#') || (!l[0]))
122 continue;
123 rpw = l;
124 w = ap_getword(r->pool, &rpw, ':');
125 x = ap_getword(r->pool, &rpw, ':');
126
127 if (x && w && !strcmp(user, w) && !strcmp(ap_auth_name(r), x)) {
128 ap_cfg_closefile(f);
129 return ap_pstrdup(r->pool, rpw);
130 }
131 }
132 ap_cfg_closefile(f);
133 return NULL;
134 }
135
136 /* Parse the Authorization header, if it exists */
137
get_digest_rec(request_rec * r,digest_header_rec * response)138 static int get_digest_rec(request_rec *r, digest_header_rec * response)
139 {
140 const char *auth_line;
141 int l;
142 int s, vk = 0, vv = 0;
143 const char *t;
144 char *key, *value;
145 const char *scheme;
146
147 if (!(t = ap_auth_type(r)) || strcasecmp(t, "Digest"))
148 return DECLINED;
149
150 if (!ap_auth_name(r)) {
151 ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_ERR, r,
152 "need AuthName: %s", r->uri);
153 return SERVER_ERROR;
154 }
155
156 auth_line = ap_table_get(r->headers_in,
157 r->proxyreq == STD_PROXY ? "Proxy-Authorization"
158 : "Authorization");
159 if (!auth_line) {
160 ap_note_digest_auth_failure(r);
161 return AUTH_REQUIRED;
162 }
163
164 if (strcasecmp(scheme = ap_getword_white(r->pool, &auth_line), "Digest")) {
165 /* Client tried to authenticate using wrong auth scheme */
166 ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_ERR, r->server,
167 "client used wrong authentication scheme: %s for %s",
168 scheme, r->uri);
169 ap_note_digest_auth_failure(r);
170 return AUTH_REQUIRED;
171 }
172
173 l = strlen(auth_line);
174
175 /* Note we don't allocate l + 1 bytes for these deliberately, because
176 * there has to be at least one '=' character for either of these two
177 * new strings to be terminated. That takes care of the need for +1.
178 */
179 key = ap_palloc(r->pool, l);
180 value = ap_palloc(r->pool, l);
181
182 /* There's probably a better way to do this, but for the time being...
183 *
184 * Right now the parsing is very 'slack'. Actual rules from RFC 2617 are:
185 *
186 * Authorization = "Digest" digest-response
187 * digest-response = 1#( username | realm | nonce | digest-uri |
188 * response | [ cnonce ] | [ algorithm ] |
189 * [opaque] | [message-qop] | [nonce-count] |
190 * [auth-param] ) (see note 4)
191 * username = "username" "=" username-value
192 * username-value = quoted-string
193 * digest-uri = "uri" "=" digest-uri-value
194 * digest-uri-value = request-uri
195 * message-qop = "qop" "=" qop-value
196 * qop-options = "qop" "=" <"> 1#qop-value <"> (see note 3)
197 * qop-value = "auth" | "auth-int" | token
198 * cnonce = "cnonce" "=" cnonce-value
199 * cnonce-value = nonce-value
200 * nonce-count = "nc" "=" nc-value
201 * nc-value = 8LHEX
202 * response = "response" "=" response-digest
203 * response-digest = <"> *LHEX <">
204 * LHEX = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" |
205 * "8" | "9" | "a" | "b" | "c" | "d" | "e" | "f"
206 *
207 * Current Discrepancies:
208 * quoted-string section 2.2 of RFC 2068
209 * --> We also acccept unquoted strings or strings
210 * like foo" bar". And take a space, comma or EOL as
211 * the terminator in that case.
212 *
213 * request-uri section 5.1 of RFC 2068
214 * --> We currently also accept any quoted string - and
215 * ignore those quotes.
216 *
217 * response/entity-digest
218 * --> We ignore the presense of the " if any.
219 *
220 * Note: There is an inherent problem with the request URI; as it should
221 * be used unquoted - yet may contain a ',' - which is used as
222 * a terminator:
223 * Authorization: Digest username="dirkx", realm="DAV", nonce="1031662894",
224 * uri=/mary,+dirkx,+peter+and+mary.ics, response="99a6275793be28c31a5b6e4467fa4c79",
225 * algorithm=MD5
226 *
227 * Note3: Taken from section 3.2.1 - as this is not actually defined in section 3.2.2
228 * which deals with the Authorization Request Header.
229 *
230 * Note4: The 'comma separated' list concept is refered to in the RFC
231 * but whitespace eating and other such things are assumed to be
232 * as per MIME/RFC2068 spec.
233 */
234
235 #define D_KEY 0
236 #define D_VALUE 1
237 #define D_STRING 2
238 #define D_EXIT -1
239
240 s = D_KEY;
241 while (s != D_EXIT) {
242 switch (s) {
243 case D_STRING:
244 if (auth_line[0] == '\"') {
245 s = D_VALUE;
246 }
247 else {
248 value[vv] = auth_line[0];
249 vv++;
250 }
251 auth_line++;
252 break;
253
254 case D_VALUE:
255 /* A request URI may be unquoted and yet
256 * contain non alpha/num chars. (Though gets terminated by
257 * a ',' - which in fact may be in the URI - so I guess
258 * 2069 should be updated to suggest strongly to quote).
259 */
260 if (auth_line[0] == '\"') {
261 s = D_STRING;
262 }
263 else if ((auth_line[0] != ',') && (auth_line[0] != ' ') && (auth_line[0] != '\0')) {
264 value[vv] = auth_line[0];
265 vv++;
266 }
267 else {
268 value[vv] = '\0';
269
270 if (!strcasecmp(key, "username"))
271 response->username = ap_pstrdup(r->pool, value);
272 else if (!strcasecmp(key, "realm"))
273 response->realm = ap_pstrdup(r->pool, value);
274 else if (!strcasecmp(key, "nonce"))
275 response->nonce = ap_pstrdup(r->pool, value);
276 else if (!strcasecmp(key, "uri"))
277 response->requested_uri = ap_pstrdup(r->pool, value);
278 else if (!strcasecmp(key, "response"))
279 response->digest = ap_pstrdup(r->pool, value);
280
281 vv = 0;
282 s = D_KEY;
283 }
284 auth_line++;
285 break;
286
287 case D_KEY:
288 if (ap_isalnum(auth_line[0])) {
289 key[vk] = auth_line[0];
290 vk++;
291 }
292 else if (auth_line[0] == '=') {
293 key[vk] = '\0';
294 vk = 0;
295 s = D_VALUE;
296 }
297 auth_line++;
298 break;
299 }
300
301 if (auth_line[-1] == '\0')
302 s = D_EXIT;
303 }
304
305 if (!response->username || !response->realm || !response->nonce ||
306 !response->requested_uri || !response->digest) {
307 ap_note_digest_auth_failure(r);
308 return AUTH_REQUIRED;
309 }
310
311 r->connection->user = response->username;
312 r->connection->ap_auth_type = "Digest";
313
314 return OK;
315 }
316
317 /* The actual MD5 code... whee */
318
319 /* Check that a given nonce is actually one which was
320 * issued by this server in the right context.
321 */
check_nonce(pool * p,const char * prefix,const char * nonce)322 static int check_nonce(pool *p, const char *prefix, const char *nonce) {
323 char *timestamp = (char *)nonce + 2 * MD5_DIGESTSIZE;
324 char *md5;
325
326 if (strlen(nonce) < MD5_DIGESTSIZE)
327 return AUTH_REQUIRED;
328
329 md5 = ap_md5(p, (unsigned char *)ap_pstrcat(p, prefix, timestamp, NULL));
330
331 return strncmp(md5, nonce, 2 * MD5_DIGESTSIZE);
332 }
333
334 /* Check the digest itself.
335 */
find_digest(request_rec * r,digest_header_rec * h,char * a1)336 static char *find_digest(request_rec *r, digest_header_rec * h, char *a1)
337 {
338 return ap_md5(r->pool,
339 (unsigned char *)ap_pstrcat(r->pool, a1, ":", h->nonce, ":",
340 ap_md5(r->pool,
341 (unsigned char *)ap_pstrcat(r->pool, r->method, ":",
342 h->requested_uri, NULL)),
343 NULL));
344 }
345
346 /* These functions return 0 if client is OK, and proper error status
347 * if not... either AUTH_REQUIRED, if we made a check, and it failed, or
348 * SERVER_ERROR, if things are so totally confused that we couldn't
349 * figure out how to tell if the client is authorized or not.
350 *
351 * If they return DECLINED, and all other modules also decline, that's
352 * treated by the server core as a configuration error, logged and
353 * reported as such.
354 */
355
356 /* Determine user ID, and check if it really is that user, for HTTP
357 * basic authentication...
358 */
359
authenticate_digest_user(request_rec * r)360 static int authenticate_digest_user(request_rec *r)
361 {
362 digest_config_rec *sec =
363 (digest_config_rec *) ap_get_module_config(r->per_dir_config,
364 &digest_module);
365 digest_header_rec *response = ap_pcalloc(r->pool, sizeof(digest_header_rec));
366 conn_rec *c = r->connection;
367 char *a1;
368 int res;
369
370 if ((res = get_digest_rec(r, response)))
371 return res;
372
373 if (!sec->pwfile)
374 return DECLINED;
375
376 /* Check that the nonce was one we actually issued. */
377 if (check_nonce(r->pool, ap_auth_nonce(r), response->nonce)) {
378 ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_ERR, r,
379 "Client is using a nonce which was not issued by "
380 "this server for this context: %s", r->uri);
381 ap_note_digest_auth_failure(r);
382 return AUTH_REQUIRED;
383 }
384
385 if (!(a1 = get_hash(r, c->user, sec->pwfile))) {
386 ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_ERR, r,
387 "user %s not found: %s", c->user, r->uri);
388 ap_note_digest_auth_failure(r);
389 return AUTH_REQUIRED;
390 }
391 if (strcmp(response->digest, find_digest(r, response, a1))) {
392 ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_ERR, r,
393 "user %s: password mismatch: %s", c->user, r->uri);
394 ap_note_digest_auth_failure(r);
395 return AUTH_REQUIRED;
396 }
397 return OK;
398 }
399
400 /* Checking ID */
401
digest_check_auth(request_rec * r)402 static int digest_check_auth(request_rec *r)
403 {
404 char *user = r->connection->user;
405 int m = r->method_number;
406 int method_restricted = 0;
407 register int x;
408 const char *t;
409 char *w;
410 const array_header *reqs_arr;
411 require_line *reqs;
412
413 if (!(t = ap_auth_type(r)) || strcasecmp(t, "Digest"))
414 return DECLINED;
415
416 reqs_arr = ap_requires(r);
417 /* If there is no "requires" directive,
418 * then any user will do.
419 */
420 if (!reqs_arr)
421 return OK;
422 reqs = (require_line *) reqs_arr->elts;
423
424 for (x = 0; x < reqs_arr->nelts; x++) {
425
426 if (!(reqs[x].method_mask & (1 << m)))
427 continue;
428
429 method_restricted = 1;
430
431 t = reqs[x].requirement;
432 w = ap_getword_white(r->pool, &t);
433 if (!strcmp(w, "valid-user"))
434 return OK;
435 else if (!strcmp(w, "user")) {
436 while (t[0]) {
437 w = ap_getword_conf(r->pool, &t);
438 if (!strcmp(user, w))
439 return OK;
440 }
441 }
442 else
443 return DECLINED;
444 }
445
446 if (!method_restricted)
447 return OK;
448
449 ap_note_digest_auth_failure(r);
450 return AUTH_REQUIRED;
451 }
452
453 module MODULE_VAR_EXPORT digest_module =
454 {
455 STANDARD_MODULE_STUFF,
456 NULL, /* initializer */
457 create_digest_dir_config, /* dir config creater */
458 NULL, /* dir merger --- default is to override */
459 NULL, /* server config */
460 NULL, /* merge server config */
461 digest_cmds, /* command table */
462 NULL, /* handlers */
463 NULL, /* filename translation */
464 authenticate_digest_user, /* check_user_id */
465 digest_check_auth, /* check auth */
466 NULL, /* check access */
467 NULL, /* type_checker */
468 NULL, /* fixups */
469 NULL, /* logger */
470 NULL, /* header parser */
471 NULL, /* child_init */
472 NULL, /* child_exit */
473 NULL /* post read-request */
474 };
475
476