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