├── LICENSE ├── bugs.txt ├── config ├── htdigest.py ├── ngx_http_auth_digest_module.c ├── ngx_http_auth_digest_module.h └── readme.rst /LICENSE: -------------------------------------------------------------------------------- 1 | Redistribution and use in source and binary forms, with or without 2 | modification, are permitted provided that the following conditions 3 | are met: 4 | 1. Redistributions of source code must retain the above copyright 5 | notice, this list of conditions and the following disclaimer. 6 | 2. Redistributions in binary form must reproduce the above copyright 7 | notice, this list of conditions and the following disclaimer in the 8 | documentation and/or other materials provided with the distribution. 9 | 10 | THIS SOFTWARE IS PROVIDED BY AUTHOR AND CONTRIBUTORS ``AS IS'' AND 11 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 12 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 13 | ARE DISCLAIMED. IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE 14 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 15 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 16 | OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 17 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 18 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 19 | OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 20 | SUCH DAMAGE. -------------------------------------------------------------------------------- /bugs.txt: -------------------------------------------------------------------------------- 1 | state management/nginx machinery 2 | - there's a fair amount of painful parsing code devoted to unpacking the key/value fields 3 | in the Authorize header. i have to believe i'm just unaware of an nginx built-in of some 4 | sort that will do this part for me. however the docs only led me to a string-level 5 | representation of the header. 6 | 7 | - there should be a directive letting you specify that only particular users in a realm may 8 | log in. how to handle wildcards though; maybe "*" or "any"? "_" or "none"? 9 | 10 | 11 | rfc 2617 12 | - currently lacks backward compatibility with clients that don't provide `qop' fields in 13 | the Authorize header. according to the rfc the server should work without it, but is it 14 | worth supporting the less secure version of an already not-bulletproof authentication 15 | scheme? 16 | 17 | - should the 401 response also offer a basic auth option if that module is also enabled 18 | for a given location block? is there a way for one module to read another's config to 19 | detect the overlap? or is this a module-loading-order issue (c.f., the way the fancy_index 20 | module inserts itself before the built-in autoindex module in its HTTP_MODULES config var)? 21 | 22 | - the opaque field is not used when generating challenges, nor is it validated when included 23 | in an authentication request. is this a significant omission? the spec makes it seem as 24 | though it only exists as a convenience to stash state in, but i could believe some software 25 | out there depends upon it... 26 | 27 | - apparently (older?) versions of internet explorer don't obey the spec when dealing with 28 | paths that contain a query string. the client should include the query in the url used 29 | to compute the digest, but IE truncates it at the question mark. see ‘current browser 30 | issues’ comment here for details: http://www.xiven.com/sourcecode/digestauthentication.php 31 | 32 | 33 | general (in)security 34 | - OOM conditions in the shm segment are not handled at all well at the moment leading to an 35 | easy DOS attack (presuming the shm size is set low enough to be exhaustible within the timeout 36 | + expire interval). Valid nonces are added to the shm and expired seconds or minutes later. 37 | Once the shm is full no new nonces can be remembered and all auth attempts will fail until 38 | enough space has been claimed through expiration. 39 | 40 | Resizing the shm at runtime is somewhat daunting so for the moment the ‘solution’ is to 41 | make sure the shm size is at the upper end of the number of requests nginx could plausibly 42 | serve within the expiration interval. 43 | 44 | -------------------------------------------------------------------------------- /config: -------------------------------------------------------------------------------- 1 | # -*- mode:sh; -*- 2 | ngx_addon_name=ngx_http_auth_digest_module 3 | 4 | if test -n "$ngx_module_link"; then 5 | ngx_module_type=HTTP 6 | ngx_module_name=ngx_http_auth_digest_module 7 | ngx_module_srcs="$ngx_addon_dir/ngx_http_auth_digest_module.c" 8 | 9 | . auto/module 10 | else 11 | HTTP_MODULES="$HTTP_MODULES ngx_http_auth_digest_module" 12 | NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_auth_digest_module.c" 13 | fi 14 | -------------------------------------------------------------------------------- /htdigest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | """ 4 | htdigest.py 5 | 6 | A barebones stand-in for the apache htdigest tool. It lacks the -c switch of the 7 | original and doesn't handle comments or blank lines. Caveat sysadmin... 8 | 9 | Created by Christian Swinehart on 2011-10-30. 10 | Copyright (c) 2011 Samizdat Drafting Co. All rights reserved. 11 | """ 12 | 13 | from __future__ import with_statement 14 | import sys 15 | import os 16 | from hashlib import md5 17 | from getpass import getpass 18 | 19 | class Passwd(object): 20 | def __init__(self, pth): 21 | super(Passwd, self).__init__() 22 | self.pth = os.path.abspath(pth) 23 | self.creds = [] 24 | if not os.path.exists(self.pth): 25 | while True: 26 | resp = raw_input('%s does not exist. Create it? (y/n) '%self.pth).lower() 27 | if resp == 'y': break 28 | if resp == 'n': sys.exit(1) 29 | else: 30 | with file(self.pth) as f: 31 | for line in f.readlines(): 32 | self.creds.append(line.strip().split(":")) 33 | 34 | def update(self, username, realm): 35 | user_matches = [c for c in self.creds if c[0]==username and c[1]==realm] 36 | if user_matches: 37 | password = getpass('Change password for "%s" to: '%username) 38 | else: 39 | password = getpass('Password for new user "%s": '%username) 40 | if password != getpass('Please repeat the password: '): 41 | print "Passwords didn't match. %s unchanged."%self.pth 42 | sys.exit(1) 43 | 44 | pw_hash = md5(':'.join([username,realm,password])).hexdigest() 45 | if user_matches: 46 | user_matches[0][2] = pw_hash 47 | else: 48 | self.creds.append([username, realm, pw_hash]) 49 | 50 | new_passwd = "\n".join(":".join(cred) for cred in self.creds) 51 | with file(self.pth,'w') as f: 52 | f.write(new_passwd) 53 | 54 | if __name__ == '__main__': 55 | if len(sys.argv) != 4: 56 | print "usage: htdigest.py passwdfile username 'realm name'" 57 | sys.exit(1) 58 | fn,user,realm = sys.argv[1:4] 59 | 60 | passwd = Passwd(fn) 61 | passwd.update(user,realm) 62 | -------------------------------------------------------------------------------- /ngx_http_auth_digest_module.c: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * copyright (c) Erik Dubbelboer 4 | * fork from nginx-http-auth-digest (c) samizdat drafting co. 5 | * derived from http_auth_basic (c) igor sysoev 6 | */ 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include "ngx_http_auth_digest_module.h" 14 | 15 | static void *ngx_http_auth_digest_create_loc_conf(ngx_conf_t *cf) { 16 | ngx_http_auth_digest_loc_conf_t *conf; 17 | 18 | conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_auth_digest_loc_conf_t)); 19 | if (conf == NULL) { 20 | return NULL; 21 | } 22 | 23 | conf->timeout = NGX_CONF_UNSET_UINT; 24 | conf->expires = NGX_CONF_UNSET_UINT; 25 | conf->drop_time = NGX_CONF_UNSET_UINT; 26 | conf->replays = NGX_CONF_UNSET_UINT; 27 | conf->evasion_time = NGX_CONF_UNSET_UINT; 28 | conf->maxtries = NGX_CONF_UNSET_UINT; 29 | return conf; 30 | } 31 | 32 | static char *ngx_http_auth_digest_merge_loc_conf(ngx_conf_t *cf, void *parent, 33 | void *child) { 34 | ngx_http_auth_digest_loc_conf_t *prev = parent; 35 | ngx_http_auth_digest_loc_conf_t *conf = child; 36 | 37 | ngx_conf_merge_sec_value(conf->timeout, prev->timeout, 60); 38 | ngx_conf_merge_sec_value(conf->expires, prev->expires, 10); 39 | ngx_conf_merge_sec_value(conf->drop_time, prev->drop_time, 300); 40 | ngx_conf_merge_value(conf->replays, prev->replays, 20); 41 | ngx_conf_merge_sec_value(conf->evasion_time, prev->evasion_time, 300); 42 | ngx_conf_merge_value(conf->maxtries, prev->maxtries, 5); 43 | 44 | if (conf->user_file.value.len == 0) { 45 | conf->user_file = prev->user_file; 46 | } 47 | 48 | if (conf->realm.value.len == 0) { 49 | conf->realm = prev->realm; 50 | } 51 | 52 | return NGX_CONF_OK; 53 | } 54 | 55 | static ngx_int_t ngx_http_auth_digest_init(ngx_conf_t *cf) { 56 | ngx_http_handler_pt *h; 57 | ngx_http_core_main_conf_t *cmcf; 58 | ngx_str_t *shm_name; 59 | 60 | cmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_core_module); 61 | 62 | h = ngx_array_push(&cmcf->phases[NGX_HTTP_ACCESS_PHASE].handlers); 63 | if (h == NULL) { 64 | return NGX_ERROR; 65 | } 66 | 67 | *h = ngx_http_auth_digest_handler; 68 | 69 | ngx_http_auth_digest_cleanup_timer = 70 | ngx_pcalloc(cf->pool, sizeof(ngx_event_t)); 71 | if (ngx_http_auth_digest_cleanup_timer == NULL) { 72 | return NGX_ERROR; 73 | } 74 | 75 | shm_name = ngx_palloc(cf->pool, sizeof *shm_name); 76 | shm_name->len = sizeof("auth_digest"); 77 | shm_name->data = (unsigned char *)"auth_digest"; 78 | 79 | if (ngx_http_auth_digest_shm_size == 0) { 80 | ngx_http_auth_digest_shm_size = 4 * 256 * ngx_pagesize; // default to 4mb 81 | } 82 | 83 | ngx_http_auth_digest_shm_zone = 84 | ngx_shared_memory_add(cf, shm_name, ngx_http_auth_digest_shm_size, 85 | &ngx_http_auth_digest_module); 86 | if (ngx_http_auth_digest_shm_zone == NULL) { 87 | return NGX_ERROR; 88 | } 89 | ngx_http_auth_digest_shm_zone->init = ngx_http_auth_digest_init_shm_zone; 90 | 91 | return NGX_OK; 92 | } 93 | 94 | static ngx_int_t ngx_http_auth_digest_worker_init(ngx_cycle_t *cycle) { 95 | if (ngx_process != NGX_PROCESS_WORKER) { 96 | return NGX_OK; 97 | } 98 | 99 | // create a cleanup queue big enough for the max number of tree nodes in the 100 | // shm 101 | ngx_http_auth_digest_cleanup_list = 102 | ngx_array_create(cycle->pool, NGX_HTTP_AUTH_DIGEST_CLEANUP_BATCH_SIZE, 103 | sizeof(ngx_rbtree_node_t *)); 104 | 105 | if (ngx_http_auth_digest_cleanup_list == NULL) { 106 | ngx_log_error(NGX_LOG_EMERG, cycle->log, 0, 107 | "Could not allocate shared memory for auth_digest"); 108 | return NGX_ERROR; 109 | } 110 | 111 | ngx_connection_t *dummy; 112 | dummy = ngx_pcalloc(cycle->pool, sizeof(ngx_connection_t)); 113 | if (dummy == NULL) 114 | return NGX_ERROR; 115 | dummy->fd = (ngx_socket_t)-1; 116 | dummy->data = cycle; 117 | 118 | ngx_http_auth_digest_cleanup_timer->log = ngx_cycle->log; 119 | ngx_http_auth_digest_cleanup_timer->data = dummy; 120 | ngx_http_auth_digest_cleanup_timer->handler = ngx_http_auth_digest_cleanup; 121 | ngx_add_timer(ngx_http_auth_digest_cleanup_timer, 122 | NGX_HTTP_AUTH_DIGEST_CLEANUP_INTERVAL); 123 | return NGX_OK; 124 | } 125 | 126 | static ngx_int_t ngx_http_auth_digest_handler(ngx_http_request_t *r) { 127 | off_t offset; 128 | ssize_t n; 129 | ngx_fd_t fd; 130 | ngx_int_t rc; 131 | ngx_err_t err; 132 | ngx_str_t user_file, passwd_line, realm; 133 | ngx_file_t file; 134 | ngx_uint_t i, begin, tail, idle; 135 | ngx_http_auth_digest_loc_conf_t *alcf; 136 | ngx_http_auth_digest_cred_t *auth_fields; 137 | u_char buf[NGX_HTTP_AUTH_DIGEST_BUF_SIZE]; 138 | u_char line[NGX_HTTP_AUTH_DIGEST_BUF_SIZE]; 139 | u_char *p; 140 | 141 | if (r->internal) { 142 | return NGX_DECLINED; 143 | } 144 | 145 | // if digest auth is disabled for this location, bail out immediately 146 | alcf = ngx_http_get_module_loc_conf(r, ngx_http_auth_digest_module); 147 | 148 | if (alcf->realm.value.len == 0) { 149 | return NGX_DECLINED; 150 | } 151 | 152 | if (ngx_http_complex_value(r, &alcf->realm, &realm) != NGX_OK) { 153 | return NGX_ERROR; 154 | } 155 | 156 | if (realm.len == 0 || alcf->user_file.value.len == 0) { 157 | return NGX_DECLINED; 158 | } 159 | 160 | if (ngx_strcmp(realm.data, "off") == 0) { 161 | return NGX_DECLINED; 162 | } 163 | 164 | if (ngx_http_auth_digest_evading(r, alcf)) { 165 | return NGX_HTTP_UNAUTHORIZED; 166 | } 167 | // unpack the Authorization header (if any) and verify that it contains all 168 | // required fields. otherwise send a challenge 169 | auth_fields = ngx_pcalloc(r->pool, sizeof(ngx_http_auth_digest_cred_t)); 170 | rc = ngx_http_auth_digest_check_credentials(r, auth_fields); 171 | if (rc == NGX_DECLINED) { 172 | return ngx_http_auth_digest_send_challenge(r, &realm, 0); 173 | } else if (rc == NGX_ERROR) { 174 | return NGX_HTTP_INTERNAL_SERVER_ERROR; 175 | } 176 | 177 | // check for the existence of a passwd file and attempt to open it 178 | if (ngx_http_complex_value(r, &alcf->user_file, &user_file) != NGX_OK) { 179 | return NGX_ERROR; 180 | } 181 | fd = ngx_open_file(user_file.data, NGX_FILE_RDONLY, NGX_FILE_OPEN, 0); 182 | if (fd == NGX_INVALID_FILE) { 183 | ngx_uint_t level; 184 | err = ngx_errno; 185 | 186 | if (err == NGX_ENOENT) { 187 | level = NGX_LOG_ERR; 188 | rc = NGX_HTTP_FORBIDDEN; 189 | 190 | } else { 191 | level = NGX_LOG_CRIT; 192 | rc = NGX_HTTP_INTERNAL_SERVER_ERROR; 193 | } 194 | 195 | ngx_log_error(level, r->connection->log, err, 196 | ngx_open_file_n " \"%s\" failed", user_file.data); 197 | return rc; 198 | } 199 | ngx_memzero(&file, sizeof(ngx_file_t)); 200 | file.fd = fd; 201 | file.name = user_file; 202 | file.log = r->connection->log; 203 | 204 | // step through the passwd file and find the individual lines, then pass them 205 | // off 206 | // to be compared against the values in the authorization header 207 | passwd_line.data = line; 208 | offset = begin = tail = 0; 209 | idle = 1; 210 | ngx_memzero(buf, NGX_HTTP_AUTH_DIGEST_BUF_SIZE); 211 | ngx_memzero(passwd_line.data, NGX_HTTP_AUTH_DIGEST_BUF_SIZE); 212 | while (1) { 213 | n = ngx_read_file(&file, buf + tail, NGX_HTTP_AUTH_DIGEST_BUF_SIZE - tail, 214 | offset); 215 | if (n == NGX_ERROR) { 216 | ngx_http_auth_digest_close(&file); 217 | return NGX_HTTP_INTERNAL_SERVER_ERROR; 218 | } 219 | 220 | begin = 0; 221 | for (i = 0; i < n + tail; i++) { 222 | if (buf[i] == '\n' || buf[i] == '\r') { 223 | if (!idle && 224 | i - begin > 225 | 36) { // 36 is the min length with a single-char name and realm 226 | p = ngx_cpymem(passwd_line.data, &buf[begin], i - begin); 227 | p[0] = '\0'; 228 | passwd_line.len = i - begin; 229 | rc = ngx_http_auth_digest_verify_user(r, auth_fields, &passwd_line); 230 | 231 | if (rc == NGX_HTTP_AUTH_DIGEST_USERNOTFOUND) { 232 | rc = NGX_DECLINED; 233 | } 234 | 235 | if (rc != NGX_DECLINED) { 236 | ngx_http_auth_digest_close(&file); 237 | ngx_http_auth_digest_evasion_tracking( 238 | r, alcf, NGX_HTTP_AUTH_DIGEST_STATUS_SUCCESS); 239 | return rc; 240 | } 241 | } 242 | idle = 1; 243 | begin = i; 244 | } else if (idle) { 245 | idle = 0; 246 | begin = i; 247 | } 248 | } 249 | 250 | if (!idle) { 251 | tail = n + tail - begin; 252 | if (n == 0 && tail > 36) { 253 | p = ngx_cpymem(passwd_line.data, &buf[begin], tail); 254 | p[0] = '\0'; 255 | passwd_line.len = i - begin; 256 | rc = ngx_http_auth_digest_verify_user(r, auth_fields, &passwd_line); 257 | if (rc == NGX_HTTP_AUTH_DIGEST_USERNOTFOUND) { 258 | rc = NGX_DECLINED; 259 | } 260 | if (rc != NGX_DECLINED) { 261 | ngx_http_auth_digest_close(&file); 262 | ngx_http_auth_digest_evasion_tracking( 263 | r, alcf, NGX_HTTP_AUTH_DIGEST_STATUS_SUCCESS); 264 | return rc; 265 | } 266 | } else { 267 | ngx_memmove(buf, &buf[begin], tail); 268 | } 269 | } 270 | 271 | if (n == 0) { 272 | break; 273 | } 274 | 275 | offset += n; 276 | } 277 | 278 | ngx_http_auth_digest_close(&file); 279 | 280 | // log only wrong username/password, 281 | // not expired hash 282 | int nc = ngx_hextoi(auth_fields->nc.data, auth_fields->nc.len); 283 | if (nc == 1) { 284 | ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, 285 | "invalid username or password for %*s", 286 | auth_fields->username.len, auth_fields->username.data); 287 | } 288 | 289 | ngx_http_auth_digest_evasion_tracking(r, alcf, 290 | NGX_HTTP_AUTH_DIGEST_STATUS_FAILURE); 291 | 292 | // since no match was found based on the fields in the authorization header, 293 | // send a new challenge and let the client retry 294 | return ngx_http_auth_digest_send_challenge(r, &realm, auth_fields->stale); 295 | } 296 | 297 | ngx_int_t 298 | ngx_http_auth_digest_check_credentials(ngx_http_request_t *r, 299 | ngx_http_auth_digest_cred_t *ctx) { 300 | 301 | if (r->headers_in.authorization == NULL) { 302 | return NGX_DECLINED; 303 | } 304 | 305 | /* 306 | token = 1* 307 | separators = "(" | ")" | "<" | ">" | "@" 308 | | "," | ";" | ":" | "\" | <"> 309 | | "/" | "[" | "]" | "?" | "=" 310 | | "{" | "}" | SP | HT 311 | */ 312 | 313 | static uint32_t token_char[] = { 314 | 0x00000000, /* 0000 0000 0000 0000 0000 0000 0000 0000 */ 315 | 316 | /* ?>=< ;:98 7654 3210 /.-, +*)( '&%$ #"! */ 317 | 0x03ff6cfa, /* 0000 0011 1111 1111 0110 1100 1111 1010 */ 318 | 319 | /* _^]\ [ZYX WVUT SRQP ONML KJIH GFED CBA@ */ 320 | 0xc7fffffe, /* 1100 0111 1111 1111 1111 1111 1111 1110 */ 321 | 322 | /* ~}| {zyx wvut srqp onml kjih gfed cba` */ 323 | 0x57ffffff, /* 0101 0111 1111 1111 1111 1111 1111 1111 */ 324 | 325 | 0x00000000, /* 0000 0000 0000 0000 0000 0000 0000 0000 */ 326 | 0x00000000, /* 0000 0000 0000 0000 0000 0000 0000 0000 */ 327 | 0x00000000, /* 0000 0000 0000 0000 0000 0000 0000 0000 */ 328 | 0x00000000, /* 0000 0000 0000 0000 0000 0000 0000 0000 */ 329 | }; 330 | 331 | uint32_t in_value; 332 | u_char ch, *p, *last, *start = 0, *end; 333 | ngx_str_t name, value; 334 | ngx_int_t comma_count = 0, quoted_pair_count = 0; 335 | 336 | name.data = 0; 337 | name.len = 0; 338 | value.data = 0; 339 | value.len = 0; 340 | 341 | enum { 342 | sw_start = 0, 343 | sw_scheme, 344 | sw_scheme_end, 345 | sw_lws_start, 346 | sw_lws, 347 | sw_param_name_start, 348 | sw_param_name, 349 | sw_param_value_start, 350 | sw_param_value, 351 | sw_param_quoted_value, 352 | sw_param_end, 353 | sw_error, 354 | } state; 355 | 356 | ngx_str_t encoded = r->headers_in.authorization->value; 357 | 358 | state = sw_start; 359 | p = encoded.data; 360 | last = encoded.data + encoded.len; 361 | 362 | ch = *p++; 363 | 364 | while (p <= last) { 365 | switch (state) { 366 | default: 367 | case sw_error: 368 | return NGX_DECLINED; 369 | 370 | /* first char */ 371 | case sw_start: 372 | if (ch == CR || ch == LF || ch == ' ' || ch == '\t') { 373 | ch = *p++; 374 | } else if (token_char[ch >> 5] & (1 << (ch & 0x1f))) { 375 | start = p - 1; 376 | state = sw_scheme; 377 | } else { 378 | state = sw_error; 379 | } 380 | break; 381 | 382 | case sw_scheme: 383 | if (token_char[ch >> 5] & (1 << (ch & 0x1f))) { 384 | ch = *p++; 385 | } else if (ch == ' ') { 386 | end = p - 1; 387 | state = sw_scheme_end; 388 | 389 | ctx->auth_scheme.data = start; 390 | ctx->auth_scheme.len = end - start; 391 | 392 | if (ngx_strncasecmp(ctx->auth_scheme.data, (u_char *)"Digest", 393 | ctx->auth_scheme.len) != 0) { 394 | state = sw_error; 395 | } 396 | } else { 397 | state = sw_error; 398 | } 399 | break; 400 | 401 | case sw_scheme_end: 402 | if (ch == ' ') { 403 | ch = *p++; 404 | } else { 405 | state = sw_param_name_start; 406 | } 407 | break; 408 | 409 | case sw_lws_start: 410 | comma_count = 0; 411 | state = sw_lws; 412 | 413 | /* fall through */ 414 | case sw_lws: 415 | if (comma_count > 0 && (token_char[ch >> 5] & (1 << (ch & 0x1f)))) { 416 | state = sw_param_name_start; 417 | } else if (ch == ',') { 418 | comma_count++; 419 | ch = *p++; 420 | } else if (ch == CR || ch == LF || ch == ' ' || ch == '\t') { 421 | ch = *p++; 422 | } else { 423 | state = sw_error; 424 | } 425 | break; 426 | 427 | case sw_param_name_start: 428 | if (token_char[ch >> 5] & (1 << (ch & 0x1f))) { 429 | start = p - 1; 430 | state = sw_param_name; 431 | ch = *p++; 432 | } else { 433 | state = sw_error; 434 | } 435 | break; 436 | 437 | case sw_param_name: 438 | if (token_char[ch >> 5] & (1 << (ch & 0x1f))) { 439 | ch = *p++; 440 | } else if (ch == '=') { 441 | end = p - 1; 442 | state = sw_param_value_start; 443 | 444 | name.data = start; 445 | name.len = end - start; 446 | 447 | ch = *p++; 448 | } else { 449 | state = sw_error; 450 | } 451 | break; 452 | 453 | case sw_param_value_start: 454 | if (token_char[ch >> 5] & (1 << (ch & 0x1f))) { 455 | start = p - 1; 456 | state = sw_param_value; 457 | ch = *p++; 458 | } else if (ch == '\"') { 459 | start = p; 460 | quoted_pair_count = 0; 461 | state = sw_param_quoted_value; 462 | ch = *p++; 463 | } else { 464 | state = sw_error; 465 | } 466 | break; 467 | 468 | case sw_param_value: 469 | in_value = token_char[ch >> 5] & (1 << (ch & 0x1f)); 470 | if (in_value) { 471 | ch = *p++; 472 | } 473 | 474 | if (!in_value || p > last) { 475 | end = p - 1; 476 | value.data = start; 477 | value.len = end - start; 478 | state = sw_param_end; 479 | goto param_end; 480 | } 481 | break; 482 | 483 | case sw_param_quoted_value: 484 | if (ch < 0x20 || ch == 0x7f) { 485 | state = sw_error; 486 | } else if (ch == '\\' && *p <= 0x7f) { 487 | quoted_pair_count++; 488 | /* Skip the next char, even if it's a \ */ 489 | ch = *(p += 2); 490 | } else if (ch == '\"') { 491 | end = p - 1; 492 | ch = *p++; 493 | value.data = start; 494 | value.len = end - start - quoted_pair_count; 495 | if (quoted_pair_count > 0) { 496 | value.data = ngx_palloc(r->pool, value.len); 497 | u_char *d = value.data; 498 | u_char *s = start; 499 | for (; s < end; s++) { 500 | ch = *s; 501 | if (ch == '\\') { 502 | /* Make sure to add the next character 503 | * even if it's a \ 504 | */ 505 | s++; 506 | if (s < end) { 507 | *d++ = ch; 508 | } 509 | continue; 510 | } 511 | *d++ = ch; 512 | } 513 | } 514 | state = sw_param_end; 515 | goto param_end; 516 | } else { 517 | ch = *p++; 518 | } 519 | break; 520 | 521 | param_end: 522 | case sw_param_end: 523 | if (ngx_strncasecmp(name.data, (u_char *)"username", name.len) == 0) { 524 | ctx->username = value; 525 | } else if (ngx_strncasecmp(name.data, (u_char *)"qop", name.len) == 0) { 526 | ctx->qop = value; 527 | } else if (ngx_strncasecmp(name.data, (u_char *)"realm", name.len) == 0) { 528 | ctx->realm = value; 529 | } else if (ngx_strncasecmp(name.data, (u_char *)"nonce", name.len) == 0) { 530 | ctx->nonce = value; 531 | } else if (ngx_strncasecmp(name.data, (u_char *)"nc", name.len) == 0) { 532 | ctx->nc = value; 533 | } else if (ngx_strncasecmp(name.data, (u_char *)"uri", name.len) == 0) { 534 | ctx->uri = value; 535 | } else if (ngx_strncasecmp(name.data, (u_char *)"cnonce", name.len) == 536 | 0) { 537 | ctx->cnonce = value; 538 | } else if (ngx_strncasecmp(name.data, (u_char *)"response", name.len) == 539 | 0) { 540 | ctx->response = value; 541 | } else if (ngx_strncasecmp(name.data, (u_char *)"opaque", name.len) == 542 | 0) { 543 | ctx->opaque = value; 544 | } 545 | 546 | state = sw_lws_start; 547 | break; 548 | } 549 | } 550 | 551 | if (state != sw_lws_start && state != sw_lws) { 552 | return NGX_DECLINED; 553 | } 554 | 555 | // bail out if anything but the opaque field is missing from the request 556 | // header 557 | if (!(ctx->username.len > 0 && ctx->qop.len > 0 && ctx->realm.len > 0 && 558 | ctx->nonce.len > 0 && ctx->nc.len > 0 && ctx->uri.len > 0 && 559 | ctx->cnonce.len > 0 && ctx->response.len > 0) || 560 | ctx->nonce.len != 16) { 561 | return NGX_DECLINED; 562 | } 563 | 564 | return NGX_OK; 565 | } 566 | 567 | static ngx_int_t 568 | ngx_http_auth_digest_verify_user(ngx_http_request_t *r, 569 | ngx_http_auth_digest_cred_t *fields, 570 | ngx_str_t *line) { 571 | ngx_uint_t i, from, nomatch; 572 | enum { sw_login, sw_ha1, sw_realm } state; 573 | 574 | state = sw_login; 575 | from = 0; 576 | nomatch = 0; 577 | 578 | // step through a single line (of the passwd file), matching the username and 579 | // realm 580 | // character-by-character against the request's Authorization header fields 581 | u_char *buf = line->data; 582 | for (i = 0; i <= line->len; i++) { 583 | u_char ch = buf[i]; 584 | 585 | switch (state) { 586 | case sw_login: 587 | if (ch == '#') 588 | nomatch = 1; 589 | if (ch == ':') { 590 | if (fields->username.len != i) 591 | nomatch = 1; 592 | state = sw_realm; 593 | from = i + 1; 594 | } else if (i > fields->username.len || ch != fields->username.data[i]) { 595 | nomatch = 1; 596 | } 597 | break; 598 | 599 | case sw_realm: 600 | if (ch == '#') 601 | nomatch = 1; 602 | if (ch == ':') { 603 | if (fields->realm.len != i - from) 604 | nomatch = 1; 605 | state = sw_ha1; 606 | from = i + 1; 607 | } else if (ch != fields->realm.data[i - from]) { 608 | nomatch = 1; 609 | } 610 | break; 611 | 612 | case sw_ha1: 613 | if (ch == '\0' || ch == ':' || ch == '#' || ch == CR || ch == LF) { 614 | if (i - from != 32) 615 | nomatch = 1; 616 | } 617 | break; 618 | } 619 | } 620 | 621 | if (nomatch) { 622 | return NGX_HTTP_AUTH_DIGEST_USERNOTFOUND; 623 | } 624 | 625 | return ngx_http_auth_digest_verify_hash(r, fields, &buf[from]); 626 | } 627 | 628 | static ngx_int_t 629 | ngx_http_auth_digest_verify_hash(ngx_http_request_t *r, 630 | ngx_http_auth_digest_cred_t *fields, 631 | u_char *hashed_pw) { 632 | u_char *p; 633 | ngx_str_t http_method; 634 | ngx_str_t HA1, HA2, ha2_key; 635 | ngx_str_t digest, digest_key; 636 | ngx_md5_t md5; 637 | u_char hash[16]; 638 | 639 | // The .net Http library sends the incorrect URI as part of the Authorization 640 | // response. Instead of the complete URI including the query parameters it 641 | // sends only the basic URI without the query parameters. It also uses this 642 | // value in the calculations. 643 | // To be compatible with the .net library the following change is made to this 644 | // module: 645 | // - Compare the URI in the Authorization (A-URI) with the request URI (R-URI). 646 | // - If A-URI and R-URI are identical verify is executed. 647 | // - If A-URI and R-URI are identical up to the '?' verify is executed 648 | // - Otherwise the check is not executed and authorization is declined 649 | if (!((r->unparsed_uri.len == fields->uri.len) && 650 | (ngx_strncmp(r->unparsed_uri.data, fields->uri.data, fields->uri.len) == 0))) 651 | { 652 | if (!((r->unparsed_uri.len > fields->uri.len) && 653 | (ngx_strncmp(r->unparsed_uri.data, fields->uri.data, fields->uri.len) == 0) && 654 | (r->unparsed_uri.data[fields->uri.len] == '?'))) 655 | { 656 | return NGX_DECLINED; 657 | } 658 | } 659 | 660 | // the hashing scheme: 661 | // digest: 662 | // MD5(MD5(username:realm:password):nonce:nc:cnonce:qop:MD5(method:uri)) 663 | // ^- HA1 ^- HA2 664 | // verify: fields->response == 665 | // MD5($hashed_pw:nonce:nc:cnonce:qop:MD5(method:uri)) 666 | 667 | // ha1 was precalculated and saved to the passwd file: 668 | // md5(username:realm:password) 669 | HA1.len = 33; 670 | HA1.data = ngx_pcalloc(r->pool, HA1.len); 671 | p = ngx_cpymem(HA1.data, hashed_pw, 32); 672 | 673 | // calculate ha2: md5(method:uri) 674 | http_method.len = r->method_name.len + 1; 675 | http_method.data = ngx_pcalloc(r->pool, http_method.len); 676 | if (http_method.data == NULL) 677 | return NGX_HTTP_INTERNAL_SERVER_ERROR; 678 | p = ngx_cpymem(http_method.data, r->method_name.data, r->method_name.len); 679 | 680 | ha2_key.len = http_method.len + fields->uri.len + 1; 681 | ha2_key.data = ngx_pcalloc(r->pool, ha2_key.len); 682 | if (ha2_key.data == NULL) 683 | return NGX_HTTP_INTERNAL_SERVER_ERROR; 684 | p = ngx_cpymem(ha2_key.data, http_method.data, http_method.len - 1); 685 | *p++ = ':'; 686 | p = ngx_cpymem(p, fields->uri.data, fields->uri.len); 687 | 688 | HA2.len = 33; 689 | HA2.data = ngx_pcalloc(r->pool, HA2.len); 690 | ngx_md5_init(&md5); 691 | ngx_md5_update(&md5, ha2_key.data, ha2_key.len - 1); 692 | ngx_md5_final(hash, &md5); 693 | ngx_hex_dump(HA2.data, hash, 16); 694 | 695 | // calculate digest: md5(ha1:nonce:nc:cnonce:qop:ha2) 696 | digest_key.len = HA1.len - 1 + fields->nonce.len + fields->nc.len + 697 | fields->cnonce.len + fields->qop.len + HA2.len - 1 + 5 + 1; 698 | digest_key.data = ngx_pcalloc(r->pool, digest_key.len); 699 | if (digest_key.data == NULL) 700 | return NGX_HTTP_INTERNAL_SERVER_ERROR; 701 | 702 | p = ngx_cpymem(digest_key.data, HA1.data, HA1.len - 1); 703 | *p++ = ':'; 704 | p = ngx_cpymem(p, fields->nonce.data, fields->nonce.len); 705 | *p++ = ':'; 706 | p = ngx_cpymem(p, fields->nc.data, fields->nc.len); 707 | *p++ = ':'; 708 | p = ngx_cpymem(p, fields->cnonce.data, fields->cnonce.len); 709 | *p++ = ':'; 710 | p = ngx_cpymem(p, fields->qop.data, fields->qop.len); 711 | *p++ = ':'; 712 | p = ngx_cpymem(p, HA2.data, HA2.len - 1); 713 | 714 | digest.len = 33; 715 | digest.data = ngx_pcalloc(r->pool, 33); 716 | if (digest.data == NULL) 717 | return NGX_HTTP_INTERNAL_SERVER_ERROR; 718 | ngx_md5_init(&md5); 719 | ngx_md5_update(&md5, digest_key.data, digest_key.len - 1); 720 | ngx_md5_final(hash, &md5); 721 | ngx_hex_dump(digest.data, hash, 16); 722 | 723 | // compare the hash of the full digest string to the response field of the 724 | // auth header 725 | // and bail out if they don't match 726 | if (fields->response.len != digest.len - 1 || 727 | ngx_memcmp(digest.data, fields->response.data, fields->response.len) != 0) 728 | return NGX_DECLINED; 729 | 730 | ngx_http_auth_digest_nonce_t nonce; 731 | ngx_uint_t key; 732 | ngx_http_auth_digest_node_t *found; 733 | ngx_slab_pool_t *shpool; 734 | ngx_http_auth_digest_loc_conf_t *alcf; 735 | ngx_table_elt_t *info_header; 736 | ngx_str_t hkey, hval; 737 | 738 | shpool = (ngx_slab_pool_t *)ngx_http_auth_digest_shm_zone->shm.addr; 739 | alcf = ngx_http_get_module_loc_conf(r, ngx_http_auth_digest_module); 740 | nonce.rnd = ngx_hextoi(fields->nonce.data, 8); 741 | nonce.t = ngx_hextoi(&fields->nonce.data[8], 8); 742 | key = ngx_crc32_short((u_char *)&nonce.rnd, sizeof nonce.rnd) ^ 743 | ngx_crc32_short((u_char *)&nonce.t, sizeof(nonce.t)); 744 | 745 | int nc = ngx_hextoi(fields->nc.data, fields->nc.len); 746 | if (nc < 0 || nc >= alcf->replays) { 747 | fields->stale = 1; 748 | return NGX_DECLINED; 749 | } 750 | 751 | // make sure nonce and nc are both valid 752 | ngx_shmtx_lock(&shpool->mutex); 753 | found = (ngx_http_auth_digest_node_t *)ngx_http_auth_digest_rbtree_find( 754 | key, ngx_http_auth_digest_rbtree->root, 755 | ngx_http_auth_digest_rbtree->sentinel); 756 | if (found != NULL) { 757 | if (found->expires <= ngx_time()) { 758 | fields->stale = 1; 759 | goto invalid; 760 | } 761 | if (!ngx_bitvector_test(found->nc, nc)) { 762 | goto invalid; 763 | } 764 | if (ngx_bitvector_test(found->nc, 0)) { 765 | // if this is the first use of this nonce, switch the expiration time from 766 | // the timeout 767 | // param to now+expires. using the 0th element of the nc vector to flag 768 | // this... 769 | ngx_bitvector_set(found->nc, 0); 770 | found->expires = ngx_time() + alcf->expires; 771 | found->drop_time = ngx_time() + alcf->drop_time; 772 | } 773 | 774 | // mark this nc as ‘used’ to prevent replays 775 | ngx_bitvector_set(found->nc, nc); 776 | 777 | // todo: if the bitvector is now ‘full’, could preemptively expire the node 778 | // from the rbtree 779 | // ngx_rbtree_delete(ngx_http_auth_digest_rbtree, found); 780 | // ngx_slab_free_locked(shpool, found); 781 | 782 | ngx_shmtx_unlock(&shpool->mutex); 783 | 784 | // recalculate the digest with a modified HA2 value (for rspauth) and emit 785 | // the 786 | // Authentication-Info header 787 | ngx_memset(ha2_key.data, 0, ha2_key.len); 788 | p = ngx_snprintf(ha2_key.data, 1 + fields->uri.len, ":%s", 789 | fields->uri.data); 790 | 791 | ngx_memset(HA2.data, 0, HA2.len); 792 | ngx_md5_init(&md5); 793 | ngx_md5_update(&md5, ha2_key.data, 1 + fields->uri.len); 794 | ngx_md5_final(hash, &md5); 795 | ngx_hex_dump(HA2.data, hash, 16); 796 | 797 | ngx_memset(digest_key.data, 0, digest_key.len); 798 | p = ngx_cpymem(digest_key.data, HA1.data, HA1.len - 1); 799 | *p++ = ':'; 800 | p = ngx_cpymem(p, fields->nonce.data, fields->nonce.len); 801 | *p++ = ':'; 802 | p = ngx_cpymem(p, fields->nc.data, fields->nc.len); 803 | *p++ = ':'; 804 | p = ngx_cpymem(p, fields->cnonce.data, fields->cnonce.len); 805 | *p++ = ':'; 806 | p = ngx_cpymem(p, fields->qop.data, fields->qop.len); 807 | *p++ = ':'; 808 | p = ngx_cpymem(p, HA2.data, HA2.len - 1); 809 | 810 | ngx_md5_init(&md5); 811 | ngx_md5_update(&md5, digest_key.data, digest_key.len - 1); 812 | ngx_md5_final(hash, &md5); 813 | ngx_hex_dump(digest.data, hash, 16); 814 | 815 | ngx_str_set(&hkey, "Authentication-Info"); 816 | // sizeof() includes the null terminator, and digest.len also counts its 817 | // null terminator 818 | hval.len = sizeof("qop=\"auth\", rspauth=\"\", cnonce=\"\", nc=") + 819 | fields->cnonce.len + fields->nc.len + digest.len - 2; 820 | hval.data = ngx_pcalloc(r->pool, hval.len + 1); 821 | if (hval.data == NULL) 822 | return NGX_HTTP_INTERNAL_SERVER_ERROR; 823 | p = ngx_snprintf(hval.data, hval.len, 824 | "qop=\"auth\", rspauth=\"%*s\", cnonce=\"%*s\", nc=%*s", 825 | digest.len - 1, digest.data, fields->cnonce.len, 826 | fields->cnonce.data, fields->nc.len, fields->nc.data); 827 | info_header = ngx_list_push(&r->headers_out.headers); 828 | if (info_header == NULL) 829 | return NGX_HTTP_INTERNAL_SERVER_ERROR; 830 | info_header->key = hkey; 831 | info_header->value = hval; 832 | info_header->hash = 1; 833 | return NGX_OK; 834 | } else { 835 | invalid: 836 | // nonce is invalid/expired or client reused an nc value. suspicious... 837 | ngx_shmtx_unlock(&shpool->mutex); 838 | return NGX_DECLINED; 839 | } 840 | } 841 | 842 | static ngx_int_t ngx_http_auth_digest_send_challenge(ngx_http_request_t *r, 843 | ngx_str_t *realm, 844 | ngx_uint_t is_stale) { 845 | ngx_str_t challenge; 846 | u_char *p; 847 | size_t realm_len = strnlen((const char *)realm->data, realm->len); 848 | 849 | r->headers_out.www_authenticate = ngx_list_push(&r->headers_out.headers); 850 | if (r->headers_out.www_authenticate == NULL) { 851 | return NGX_HTTP_INTERNAL_SERVER_ERROR; 852 | } 853 | 854 | r->headers_out.www_authenticate->hash = 1; 855 | ngx_str_set(&r->headers_out.www_authenticate->key, "WWW-Authenticate"); 856 | 857 | challenge.len = 858 | sizeof("Digest algorithm=\"MD5\", qop=\"auth\", realm=\"\", nonce=\"\"") - 859 | 1 + realm_len + 16; 860 | if (is_stale) { 861 | challenge.len += sizeof(", stale=\"true\"") - 1; 862 | } 863 | challenge.data = ngx_pnalloc(r->pool, challenge.len); 864 | if (challenge.data == NULL) { 865 | return NGX_HTTP_INTERNAL_SERVER_ERROR; 866 | } 867 | 868 | ngx_http_auth_digest_nonce_t nonce; 869 | nonce = ngx_http_auth_digest_next_nonce(r); 870 | if (nonce.t == 0 && nonce.rnd == 0) { 871 | // oom error when allocating nonce session in rbtree 872 | return NGX_HTTP_SERVICE_UNAVAILABLE; 873 | } 874 | 875 | p = ngx_cpymem( 876 | challenge.data, "Digest algorithm=\"MD5\", qop=\"auth\", realm=\"", 877 | sizeof("Digest algorithm=\"MD5\", qop=\"auth\", realm=\"") - 1); 878 | p = ngx_cpymem(p, realm->data, realm_len); 879 | p = ngx_cpymem(p, "\", nonce=\"", sizeof("\", nonce=\"") - 1); 880 | p = ngx_sprintf(p, "%08xl%08xl", nonce.rnd, nonce.t); 881 | 882 | if (is_stale) { 883 | p = ngx_cpymem(p, "\", stale=\"true\"", sizeof("\", stale=\"true\"")); 884 | } else { 885 | p = ngx_cpymem(p, "\"", sizeof("\"")); 886 | } 887 | r->headers_out.www_authenticate->value = challenge; 888 | 889 | return NGX_HTTP_UNAUTHORIZED; 890 | } 891 | 892 | static void ngx_http_auth_digest_close(ngx_file_t *file) { 893 | if (ngx_close_file(file->fd) == NGX_FILE_ERROR) { 894 | ngx_log_error(NGX_LOG_ALERT, file->log, ngx_errno, 895 | ngx_close_file_n " \"%s\" failed", file->name.data); 896 | } 897 | } 898 | 899 | static char *ngx_http_auth_digest_set_user_file(ngx_conf_t *cf, 900 | ngx_command_t *cmd, 901 | void *conf) { 902 | ngx_http_auth_digest_loc_conf_t *alcf = conf; 903 | 904 | ngx_str_t *value; 905 | ngx_http_compile_complex_value_t ccv; 906 | 907 | if (alcf->user_file.value.len) { 908 | return "is duplicate"; 909 | } 910 | 911 | value = cf->args->elts; 912 | 913 | ngx_memzero(&ccv, sizeof(ngx_http_compile_complex_value_t)); 914 | 915 | ccv.cf = cf; 916 | ccv.value = &value[1]; 917 | ccv.complex_value = &alcf->user_file; 918 | ccv.zero = 1; 919 | ccv.conf_prefix = 1; 920 | 921 | if (ngx_http_compile_complex_value(&ccv) != NGX_OK) { 922 | return NGX_CONF_ERROR; 923 | } 924 | 925 | return NGX_CONF_OK; 926 | } 927 | 928 | static char *ngx_http_auth_digest_set_realm(ngx_conf_t *cf, ngx_command_t *cmd, 929 | void *conf) { 930 | ngx_http_auth_digest_loc_conf_t *alcf = conf; 931 | 932 | ngx_str_t *value; 933 | ngx_http_compile_complex_value_t ccv; 934 | 935 | if (alcf->realm.value.len) { 936 | return "is duplicate"; 937 | } 938 | 939 | value = cf->args->elts; 940 | 941 | ngx_memzero(&ccv, sizeof(ngx_http_compile_complex_value_t)); 942 | 943 | ccv.cf = cf; 944 | ccv.value = &value[1]; 945 | ccv.complex_value = &alcf->realm; 946 | ccv.zero = 1; 947 | ccv.conf_prefix = 0; 948 | ccv.root_prefix = 0; 949 | 950 | if (ngx_http_compile_complex_value(&ccv) != NGX_OK) { 951 | return NGX_CONF_ERROR; 952 | } 953 | 954 | return NGX_CONF_OK; 955 | } 956 | 957 | static char *ngx_http_auth_digest_set_shm_size(ngx_conf_t *cf, 958 | ngx_command_t *cmd, void *conf) { 959 | ssize_t new_shm_size; 960 | ngx_str_t *value; 961 | 962 | value = cf->args->elts; 963 | 964 | new_shm_size = ngx_parse_size(&value[1]); 965 | if (new_shm_size == NGX_ERROR) { 966 | ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "Invalid memory area size `%V'", 967 | &value[1]); 968 | return NGX_CONF_ERROR; 969 | } 970 | 971 | new_shm_size = ngx_align(new_shm_size, ngx_pagesize); 972 | 973 | if (new_shm_size < 8 * (ssize_t)ngx_pagesize) { 974 | ngx_conf_log_error(NGX_LOG_WARN, cf, 0, 975 | "The auth_digest_shm_size value must be at least %udKiB", 976 | (8 * ngx_pagesize) >> 10); 977 | new_shm_size = 8 * ngx_pagesize; 978 | } 979 | 980 | if (ngx_http_auth_digest_shm_size && 981 | ngx_http_auth_digest_shm_size != (ngx_uint_t)new_shm_size) { 982 | ngx_conf_log_error( 983 | NGX_LOG_WARN, cf, 0, 984 | "Cannot change memory area size without restart, ignoring change"); 985 | } else { 986 | ngx_http_auth_digest_shm_size = new_shm_size; 987 | } 988 | ngx_conf_log_error(NGX_LOG_DEBUG, cf, 0, 989 | "Using %udKiB of shared memory for auth_digest", 990 | new_shm_size >> 10); 991 | return NGX_CONF_OK; 992 | } 993 | 994 | static ngx_int_t ngx_http_auth_digest_init_shm_zone(ngx_shm_zone_t *shm_zone, 995 | void *data) { 996 | ngx_slab_pool_t *shpool; 997 | ngx_rbtree_t *tree; 998 | ngx_rbtree_node_t *sentinel; 999 | ngx_atomic_t *lock; 1000 | if (data) { 1001 | shm_zone->data = data; 1002 | return NGX_OK; 1003 | } 1004 | 1005 | shpool = (ngx_slab_pool_t *)shm_zone->shm.addr; 1006 | tree = ngx_slab_alloc(shpool, sizeof *tree); 1007 | if (tree == NULL) { 1008 | return NGX_ERROR; 1009 | } 1010 | 1011 | sentinel = ngx_slab_alloc(shpool, sizeof *sentinel); 1012 | if (sentinel == NULL) { 1013 | return NGX_ERROR; 1014 | } 1015 | 1016 | ngx_rbtree_init(tree, sentinel, ngx_http_auth_digest_rbtree_insert); 1017 | shm_zone->data = tree; 1018 | ngx_http_auth_digest_rbtree = tree; 1019 | 1020 | tree = ngx_slab_alloc(shpool, sizeof *tree); 1021 | if (tree == NULL) { 1022 | return NGX_ERROR; 1023 | } 1024 | 1025 | sentinel = ngx_slab_alloc(shpool, sizeof *sentinel); 1026 | if (sentinel == NULL) { 1027 | return NGX_ERROR; 1028 | } 1029 | 1030 | ngx_rbtree_init(tree, sentinel, ngx_http_auth_digest_ev_rbtree_insert); 1031 | ngx_http_auth_digest_ev_rbtree = tree; 1032 | 1033 | lock = ngx_slab_alloc(shpool, sizeof(ngx_atomic_t)); 1034 | if (lock == NULL) { 1035 | return NGX_ERROR; 1036 | } 1037 | ngx_http_auth_digest_cleanup_lock = lock; 1038 | 1039 | return NGX_OK; 1040 | } 1041 | 1042 | static int ngx_http_auth_digest_rbtree_cmp(const ngx_rbtree_node_t *v_left, 1043 | const ngx_rbtree_node_t *v_right) { 1044 | if (v_left->key == v_right->key) 1045 | return 0; 1046 | else 1047 | return (v_left->key < v_right->key) ? -1 : 1; 1048 | } 1049 | 1050 | static int 1051 | ngx_http_auth_digest_ev_rbtree_cmp(const ngx_rbtree_node_t *v_left, 1052 | const ngx_rbtree_node_t *v_right) { 1053 | if (v_left->key == v_right->key) { 1054 | ngx_http_auth_digest_ev_node_t *evleft = 1055 | (ngx_http_auth_digest_ev_node_t *)v_left; 1056 | ngx_http_auth_digest_ev_node_t *evright = 1057 | (ngx_http_auth_digest_ev_node_t *)v_right; 1058 | return ngx_http_auth_digest_srcaddr_cmp( 1059 | &evleft->src_addr, evleft->src_addrlen, &evright->src_addr, 1060 | evright->src_addrlen); 1061 | } 1062 | return (v_left->key < v_right->key) ? -1 : 1; 1063 | } 1064 | 1065 | static void 1066 | ngx_rbtree_generic_insert(ngx_rbtree_node_t *temp, ngx_rbtree_node_t *node, 1067 | ngx_rbtree_node_t *sentinel, 1068 | int (*compare)(const ngx_rbtree_node_t *left, 1069 | const ngx_rbtree_node_t *right)) { 1070 | for (;;) { 1071 | if (node->key < temp->key) { 1072 | 1073 | if (temp->left == sentinel) { 1074 | temp->left = node; 1075 | break; 1076 | } 1077 | 1078 | temp = temp->left; 1079 | 1080 | } else if (node->key > temp->key) { 1081 | 1082 | if (temp->right == sentinel) { 1083 | temp->right = node; 1084 | break; 1085 | } 1086 | 1087 | temp = temp->right; 1088 | 1089 | } else { /* node->key == temp->key */ 1090 | if (compare(node, temp) < 0) { 1091 | 1092 | if (temp->left == sentinel) { 1093 | temp->left = node; 1094 | break; 1095 | } 1096 | 1097 | temp = temp->left; 1098 | 1099 | } else { 1100 | 1101 | if (temp->right == sentinel) { 1102 | temp->right = node; 1103 | break; 1104 | } 1105 | 1106 | temp = temp->right; 1107 | } 1108 | } 1109 | } 1110 | 1111 | node->parent = temp; 1112 | node->left = sentinel; 1113 | node->right = sentinel; 1114 | ngx_rbt_red(node); 1115 | } 1116 | 1117 | static void ngx_http_auth_digest_rbtree_insert(ngx_rbtree_node_t *temp, 1118 | ngx_rbtree_node_t *node, 1119 | ngx_rbtree_node_t *sentinel) { 1120 | 1121 | ngx_rbtree_generic_insert(temp, node, sentinel, 1122 | ngx_http_auth_digest_rbtree_cmp); 1123 | } 1124 | 1125 | static void ngx_http_auth_digest_ev_rbtree_insert(ngx_rbtree_node_t *temp, 1126 | ngx_rbtree_node_t *node, 1127 | ngx_rbtree_node_t *sentinel) { 1128 | 1129 | ngx_rbtree_generic_insert(temp, node, sentinel, 1130 | ngx_http_auth_digest_ev_rbtree_cmp); 1131 | } 1132 | 1133 | static ngx_rbtree_node_t * 1134 | ngx_http_auth_digest_rbtree_find(ngx_rbtree_key_t key, ngx_rbtree_node_t *node, 1135 | ngx_rbtree_node_t *sentinel) { 1136 | 1137 | if (node == sentinel) 1138 | return NULL; 1139 | 1140 | ngx_rbtree_node_t *found = (node->key == key) ? node : NULL; 1141 | if (found == NULL && node->left != sentinel) { 1142 | found = ngx_http_auth_digest_rbtree_find(key, node->left, sentinel); 1143 | } 1144 | if (found == NULL && node->right != sentinel) { 1145 | found = ngx_http_auth_digest_rbtree_find(key, node->right, sentinel); 1146 | } 1147 | 1148 | return found; 1149 | } 1150 | 1151 | static ngx_http_auth_digest_ev_node_t * 1152 | ngx_http_auth_digest_ev_rbtree_find(ngx_http_auth_digest_ev_node_t *this, 1153 | ngx_rbtree_node_t *node, 1154 | ngx_rbtree_node_t *sentinel) { 1155 | int cmpval; 1156 | if (node == sentinel) 1157 | return NULL; 1158 | 1159 | cmpval = ngx_http_auth_digest_ev_rbtree_cmp((ngx_rbtree_node_t *)this, node); 1160 | if (cmpval == 0) { 1161 | return (ngx_http_auth_digest_ev_node_t *)node; 1162 | } 1163 | return ngx_http_auth_digest_ev_rbtree_find( 1164 | this, (cmpval < 0) ? node->left : node->right, sentinel); 1165 | } 1166 | 1167 | void ngx_http_auth_digest_cleanup(ngx_event_t *ev) { 1168 | if (ev->timer_set) 1169 | ngx_del_timer(ev); 1170 | 1171 | if (!(ngx_quit || ngx_terminate || ngx_exiting)) { 1172 | ngx_add_timer(ev, NGX_HTTP_AUTH_DIGEST_CLEANUP_INTERVAL); 1173 | } 1174 | 1175 | if (ngx_trylock(ngx_http_auth_digest_cleanup_lock)) { 1176 | ngx_http_auth_digest_rbtree_prune(ev->log); 1177 | ngx_http_auth_digest_ev_rbtree_prune(ev->log); 1178 | ngx_unlock(ngx_http_auth_digest_cleanup_lock); 1179 | } 1180 | } 1181 | 1182 | static void ngx_http_auth_digest_rbtree_prune(ngx_log_t *log) { 1183 | ngx_uint_t i; 1184 | time_t now = ngx_time(); 1185 | ngx_slab_pool_t *shpool = 1186 | (ngx_slab_pool_t *)ngx_http_auth_digest_shm_zone->shm.addr; 1187 | 1188 | ngx_shmtx_lock(&shpool->mutex); 1189 | ngx_http_auth_digest_cleanup_list->nelts = 0; 1190 | ngx_http_auth_digest_rbtree_prune_walk(ngx_http_auth_digest_rbtree->root, 1191 | ngx_http_auth_digest_rbtree->sentinel, 1192 | now, log); 1193 | 1194 | ngx_rbtree_node_t **elts = 1195 | (ngx_rbtree_node_t **)ngx_http_auth_digest_cleanup_list->elts; 1196 | for (i = 0; i < ngx_http_auth_digest_cleanup_list->nelts; i++) { 1197 | ngx_rbtree_delete(ngx_http_auth_digest_rbtree, elts[i]); 1198 | ngx_slab_free_locked(shpool, elts[i]); 1199 | } 1200 | ngx_shmtx_unlock(&shpool->mutex); 1201 | 1202 | // if the cleanup array grew during the run, shrink it back down 1203 | if (ngx_http_auth_digest_cleanup_list->nalloc > 1204 | NGX_HTTP_AUTH_DIGEST_CLEANUP_BATCH_SIZE) { 1205 | ngx_array_t *old_list = ngx_http_auth_digest_cleanup_list; 1206 | ngx_array_t *new_list = ngx_array_create( 1207 | old_list->pool, NGX_HTTP_AUTH_DIGEST_CLEANUP_BATCH_SIZE, 1208 | sizeof(ngx_rbtree_node_t *)); 1209 | if (new_list != NULL) { 1210 | ngx_array_destroy(old_list); 1211 | ngx_http_auth_digest_cleanup_list = new_list; 1212 | } else { 1213 | ngx_log_error(NGX_LOG_ERR, log, 0, 1214 | "auth_digest ran out of cleanup space"); 1215 | } 1216 | } 1217 | } 1218 | 1219 | static void ngx_http_auth_digest_rbtree_prune_walk(ngx_rbtree_node_t *node, 1220 | ngx_rbtree_node_t *sentinel, 1221 | time_t now, ngx_log_t *log) { 1222 | if (node == sentinel) 1223 | return; 1224 | 1225 | if (node->left != sentinel) { 1226 | ngx_http_auth_digest_rbtree_prune_walk(node->left, sentinel, now, log); 1227 | } 1228 | 1229 | if (node->right != sentinel) { 1230 | ngx_http_auth_digest_rbtree_prune_walk(node->right, sentinel, now, log); 1231 | } 1232 | 1233 | ngx_http_auth_digest_node_t *dnode = (ngx_http_auth_digest_node_t *)node; 1234 | if (dnode->drop_time <= ngx_time()) { 1235 | ngx_rbtree_node_t **dropnode = 1236 | ngx_array_push(ngx_http_auth_digest_cleanup_list); 1237 | dropnode[0] = node; 1238 | } 1239 | } 1240 | 1241 | static void ngx_http_auth_digest_ev_rbtree_prune(ngx_log_t *log) { 1242 | ngx_uint_t i; 1243 | time_t now = ngx_time(); 1244 | ngx_slab_pool_t *shpool = 1245 | (ngx_slab_pool_t *)ngx_http_auth_digest_shm_zone->shm.addr; 1246 | 1247 | ngx_shmtx_lock(&shpool->mutex); 1248 | ngx_http_auth_digest_cleanup_list->nelts = 0; 1249 | ngx_http_auth_digest_ev_rbtree_prune_walk( 1250 | ngx_http_auth_digest_ev_rbtree->root, 1251 | ngx_http_auth_digest_ev_rbtree->sentinel, now, log); 1252 | 1253 | ngx_rbtree_node_t **elts = 1254 | (ngx_rbtree_node_t **)ngx_http_auth_digest_cleanup_list->elts; 1255 | for (i = 0; i < ngx_http_auth_digest_cleanup_list->nelts; i++) { 1256 | ngx_rbtree_delete(ngx_http_auth_digest_ev_rbtree, elts[i]); 1257 | ngx_slab_free_locked(shpool, elts[i]); 1258 | } 1259 | ngx_shmtx_unlock(&shpool->mutex); 1260 | 1261 | // if the cleanup array grew during the run, shrink it back down 1262 | if (ngx_http_auth_digest_cleanup_list->nalloc > 1263 | NGX_HTTP_AUTH_DIGEST_CLEANUP_BATCH_SIZE) { 1264 | ngx_array_t *old_list = ngx_http_auth_digest_cleanup_list; 1265 | ngx_array_t *new_list = ngx_array_create( 1266 | old_list->pool, NGX_HTTP_AUTH_DIGEST_CLEANUP_BATCH_SIZE, 1267 | sizeof(ngx_rbtree_node_t *)); 1268 | if (new_list != NULL) { 1269 | ngx_array_destroy(old_list); 1270 | ngx_http_auth_digest_cleanup_list = new_list; 1271 | } else { 1272 | ngx_log_error(NGX_LOG_ERR, log, 0, 1273 | "auth_digest ran out of cleanup space"); 1274 | } 1275 | } 1276 | } 1277 | 1278 | static void 1279 | ngx_http_auth_digest_ev_rbtree_prune_walk(ngx_rbtree_node_t *node, 1280 | ngx_rbtree_node_t *sentinel, 1281 | time_t now, ngx_log_t *log) { 1282 | if (node == sentinel) 1283 | return; 1284 | 1285 | if (node->left != sentinel) { 1286 | ngx_http_auth_digest_ev_rbtree_prune_walk(node->left, sentinel, now, log); 1287 | } 1288 | 1289 | if (node->right != sentinel) { 1290 | ngx_http_auth_digest_ev_rbtree_prune_walk(node->right, sentinel, now, log); 1291 | } 1292 | 1293 | ngx_http_auth_digest_ev_node_t *dnode = 1294 | (ngx_http_auth_digest_ev_node_t *)node; 1295 | if (dnode->drop_time <= ngx_time()) { 1296 | ngx_rbtree_node_t **dropnode = 1297 | ngx_array_push(ngx_http_auth_digest_cleanup_list); 1298 | dropnode[0] = node; 1299 | } 1300 | } 1301 | 1302 | static ngx_http_auth_digest_nonce_t 1303 | ngx_http_auth_digest_next_nonce(ngx_http_request_t *r) { 1304 | ngx_http_auth_digest_loc_conf_t *alcf; 1305 | ngx_slab_pool_t *shpool; 1306 | ngx_http_auth_digest_nonce_t nonce; 1307 | ngx_uint_t key; 1308 | ngx_http_auth_digest_node_t *node; 1309 | 1310 | shpool = (ngx_slab_pool_t *)ngx_http_auth_digest_shm_zone->shm.addr; 1311 | alcf = ngx_http_get_module_loc_conf(r, ngx_http_auth_digest_module); 1312 | 1313 | // create a nonce value that's not in the active set 1314 | while (1) { 1315 | nonce.t = ngx_time(); 1316 | nonce.rnd = ngx_random(); 1317 | key = ngx_crc32_short((u_char *)&nonce.rnd, sizeof nonce.rnd) ^ 1318 | ngx_crc32_short((u_char *)&nonce.t, sizeof(nonce.t)); 1319 | 1320 | ngx_shmtx_lock(&shpool->mutex); 1321 | ngx_rbtree_node_t *found = 1322 | ngx_http_auth_digest_rbtree_find(key, ngx_http_auth_digest_rbtree->root, 1323 | ngx_http_auth_digest_rbtree->sentinel); 1324 | 1325 | if (found != NULL) { 1326 | ngx_shmtx_unlock(&shpool->mutex); 1327 | continue; 1328 | } 1329 | 1330 | node = ngx_slab_alloc_locked(shpool, 1331 | sizeof(ngx_http_auth_digest_node_t) + 1332 | ngx_bitvector_size(1 + alcf->replays)); 1333 | if (node == NULL) { 1334 | ngx_shmtx_unlock(&shpool->mutex); 1335 | ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, 1336 | "auth_digest ran out of shm space. Increase the " 1337 | "auth_digest_shm_size limit."); 1338 | nonce.t = 0; 1339 | nonce.rnd = 0; 1340 | return nonce; 1341 | } 1342 | node->expires = nonce.t + alcf->timeout; 1343 | node->drop_time = nonce.t + alcf->timeout; 1344 | ngx_memset(node->nc, 0xff, ngx_bitvector_size(1 + alcf->replays)); 1345 | ((ngx_rbtree_node_t *)node)->key = key; 1346 | ngx_rbtree_insert(ngx_http_auth_digest_rbtree, &node->node); 1347 | 1348 | ngx_shmtx_unlock(&shpool->mutex); 1349 | return nonce; 1350 | } 1351 | } 1352 | 1353 | static int ngx_http_auth_digest_srcaddr_key(struct sockaddr *sa, socklen_t len, 1354 | ngx_uint_t *key) { 1355 | struct sockaddr_in *sin; 1356 | #if (NGX_HAVE_INET6) 1357 | struct sockaddr_in6 *s6; 1358 | #endif 1359 | 1360 | switch (sa->sa_family) { 1361 | case AF_INET: 1362 | sin = (struct sockaddr_in *)sa; 1363 | *key = ngx_crc32_short((u_char *)&sin->sin_addr, sizeof(sin->sin_addr)); 1364 | return 1; 1365 | #if (NGX_HAVE_INET6) 1366 | case AF_INET6: 1367 | s6 = (struct sockaddr_in6 *)sa; 1368 | *key = ngx_crc32_short((u_char *)&s6->sin6_addr, sizeof(s6->sin6_addr)); 1369 | return 1; 1370 | #endif 1371 | default: 1372 | break; 1373 | } 1374 | return 0; 1375 | } 1376 | 1377 | static int ngx_http_auth_digest_srcaddr_cmp(struct sockaddr *sa1, 1378 | socklen_t len1, 1379 | struct sockaddr *sa2, 1380 | socklen_t len2) { 1381 | struct sockaddr_in *sin1, *sin2; 1382 | #if (NGX_HAVE_INET6) 1383 | struct sockaddr_in6 *s61, *s62; 1384 | #endif 1385 | if (len1 != len2) { 1386 | return (len1 < len2) ? -1 : 1; 1387 | } 1388 | if (sa1->sa_family != sa2->sa_family) { 1389 | return (sa1->sa_family < sa2->sa_family) ? -1 : 1; 1390 | } 1391 | 1392 | switch (sa1->sa_family) { 1393 | case AF_INET: 1394 | sin1 = (struct sockaddr_in *)sa1; 1395 | sin2 = (struct sockaddr_in *)sa2; 1396 | return ngx_memcmp(&sin1->sin_addr, &sin2->sin_addr, sizeof(sin1->sin_addr)); 1397 | #if (NGX_HAVE_INET6) 1398 | case AF_INET6: 1399 | s61 = (struct sockaddr_in6 *)sa1; 1400 | s62 = (struct sockaddr_in6 *)sa2; 1401 | return ngx_memcmp(&s61->sin6_addr, &s62->sin6_addr, sizeof(s61->sin6_addr)); 1402 | #endif 1403 | default: 1404 | break; 1405 | } 1406 | return -999; 1407 | } 1408 | 1409 | static void 1410 | ngx_http_auth_digest_evasion_tracking(ngx_http_request_t *r, 1411 | ngx_http_auth_digest_loc_conf_t *alcf, 1412 | ngx_int_t status) { 1413 | ngx_slab_pool_t *shpool; 1414 | ngx_uint_t key; 1415 | ngx_http_auth_digest_ev_node_t testnode, *node; 1416 | 1417 | if (!ngx_http_auth_digest_srcaddr_key(r->connection->sockaddr, 1418 | r->connection->socklen, &key)) { 1419 | ngx_log_error(NGX_LOG_INFO, r->connection->log, 0, 1420 | "skipping evasive tactics for this source address"); 1421 | return; 1422 | } 1423 | shpool = (ngx_slab_pool_t *)ngx_http_auth_digest_shm_zone->shm.addr; 1424 | 1425 | ngx_shmtx_lock(&shpool->mutex); 1426 | ngx_memzero(&testnode, sizeof(testnode)); 1427 | testnode.node.key = key; 1428 | ngx_memcpy(&testnode.src_addr, r->connection->sockaddr, 1429 | r->connection->socklen); 1430 | testnode.src_addrlen = r->connection->socklen; 1431 | node = ngx_http_auth_digest_ev_rbtree_find( 1432 | &testnode, ngx_http_auth_digest_ev_rbtree->root, 1433 | ngx_http_auth_digest_ev_rbtree->sentinel); 1434 | if (node == NULL) { 1435 | // Don't bother creating a node if this was a successful auth 1436 | if (status == NGX_HTTP_AUTH_DIGEST_STATUS_SUCCESS) { 1437 | ngx_log_error(NGX_LOG_INFO, r->connection->log, 0, 1438 | "sucessful auth, not tracking"); 1439 | ngx_shmtx_unlock(&shpool->mutex); 1440 | return; 1441 | } 1442 | ngx_log_error(NGX_LOG_INFO, r->connection->log, 0, "adding tracking node"); 1443 | node = 1444 | ngx_slab_alloc_locked(shpool, sizeof(ngx_http_auth_digest_ev_node_t)); 1445 | if (node == NULL) { 1446 | ngx_shmtx_unlock(&shpool->mutex); 1447 | ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, 1448 | "auth_digest ran out of shm space. Increase the " 1449 | "auth_digest_shm_size limit."); 1450 | return; 1451 | } 1452 | ngx_memcpy(&node->src_addr, r->connection->sockaddr, 1453 | r->connection->socklen); 1454 | node->src_addrlen = r->connection->socklen; 1455 | ((ngx_rbtree_node_t *)node)->key = key; 1456 | ngx_rbtree_insert(ngx_http_auth_digest_ev_rbtree, &node->node); 1457 | } 1458 | if (status == NGX_HTTP_AUTH_DIGEST_STATUS_SUCCESS) { 1459 | ngx_log_error(NGX_LOG_INFO, r->connection->log, 0, 1460 | "successful auth, clearing evasion counters"); 1461 | node->failcount = 0; 1462 | node->drop_time = ngx_time(); 1463 | } else { 1464 | // Reset the failure count to 1 if we're outside the evasion window 1465 | if (ngx_time() > node->drop_time) { 1466 | node->failcount = 1; 1467 | } else { 1468 | node->failcount += 1; 1469 | } 1470 | node->drop_time = ngx_time() + alcf->evasion_time; 1471 | ngx_log_error(NGX_LOG_INFO, r->connection->log, 0, 1472 | "failed auth, updating failcount=%d, drop_time=%d", 1473 | node->failcount, node->drop_time); 1474 | } 1475 | ngx_shmtx_unlock(&shpool->mutex); 1476 | } 1477 | 1478 | static int ngx_http_auth_digest_evading(ngx_http_request_t *r, 1479 | ngx_http_auth_digest_loc_conf_t *alcf) { 1480 | ngx_slab_pool_t *shpool; 1481 | ngx_uint_t key; 1482 | ngx_http_auth_digest_ev_node_t testnode, *node; 1483 | int evading = 0; 1484 | 1485 | if (!ngx_http_auth_digest_srcaddr_key(r->connection->sockaddr, 1486 | r->connection->socklen, &key)) { 1487 | return 0; 1488 | } 1489 | 1490 | ngx_memzero(&testnode, sizeof(testnode)); 1491 | testnode.node.key = key; 1492 | ngx_memcpy(&testnode.src_addr, r->connection->sockaddr, 1493 | r->connection->socklen); 1494 | testnode.src_addrlen = r->connection->socklen; 1495 | 1496 | shpool = (ngx_slab_pool_t *)ngx_http_auth_digest_shm_zone->shm.addr; 1497 | 1498 | ngx_shmtx_lock(&shpool->mutex); 1499 | node = ngx_http_auth_digest_ev_rbtree_find( 1500 | &testnode, ngx_http_auth_digest_ev_rbtree->root, 1501 | ngx_http_auth_digest_ev_rbtree->sentinel); 1502 | if (node != NULL && node->failcount >= alcf->maxtries && 1503 | ngx_time() < node->drop_time) { 1504 | ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, 1505 | "ignoring authentication request - in evasion period"); 1506 | evading = 1; 1507 | } 1508 | ngx_shmtx_unlock(&shpool->mutex); 1509 | return evading; 1510 | } 1511 | -------------------------------------------------------------------------------- /ngx_http_auth_digest_module.h: -------------------------------------------------------------------------------- 1 | #ifndef _NGX_HTTP_AUTH_DIGEST_H_INCLUDED_ 2 | #define _NGX_HTTP_AUTH_DIGEST_H_INCLUDED_ 3 | 4 | #define NGX_HTTP_AUTH_DIGEST_USERNOTFOUND 1000 5 | 6 | // the module conf 7 | typedef struct { 8 | ngx_http_complex_value_t realm; 9 | time_t timeout; 10 | time_t expires; 11 | time_t drop_time; 12 | time_t evasion_time; 13 | ngx_int_t replays; 14 | ngx_int_t maxtries; 15 | ngx_http_complex_value_t user_file; 16 | ngx_str_t cache_dir; 17 | } ngx_http_auth_digest_loc_conf_t; 18 | 19 | // contents of the request's authorization header 20 | typedef struct { 21 | ngx_str_t auth_scheme; 22 | ngx_str_t username; 23 | ngx_str_t realm; 24 | ngx_str_t nonce; 25 | ngx_str_t nc; 26 | ngx_str_t uri; 27 | ngx_str_t qop; 28 | ngx_str_t cnonce; 29 | ngx_str_t response; 30 | ngx_str_t opaque; 31 | ngx_int_t stale; 32 | } ngx_http_auth_digest_cred_t; 33 | 34 | // the nonce as an issue-time/random-num pair 35 | typedef struct { 36 | ngx_uint_t rnd; 37 | time_t t; 38 | } ngx_http_auth_digest_nonce_t; 39 | 40 | // nonce entries in the rbtree 41 | typedef struct { 42 | ngx_rbtree_node_t node; // the node's .key is derived from the nonce val 43 | time_t expires; // time at which the node should be evicted 44 | time_t drop_time; 45 | char nc[0]; // bitvector of used nc values to prevent replays 46 | } ngx_http_auth_digest_node_t; 47 | 48 | // evasion entries in the rbtree 49 | typedef struct { 50 | ngx_rbtree_node_t node; // the node's .key is derived from the source address 51 | time_t drop_time; 52 | ngx_int_t failcount; 53 | struct sockaddr src_addr; 54 | socklen_t src_addrlen; 55 | } ngx_http_auth_digest_ev_node_t; 56 | 57 | // the main event 58 | static ngx_int_t ngx_http_auth_digest_handler(ngx_http_request_t *r); 59 | 60 | // realm handling 61 | static char *ngx_http_auth_digest_set_realm(ngx_conf_t *cf, ngx_command_t *cmd, 62 | void *conf); 63 | 64 | // passwd file handling 65 | static void ngx_http_auth_digest_close(ngx_file_t *file); 66 | static char *ngx_http_auth_digest_set_user_file(ngx_conf_t *cf, 67 | ngx_command_t *cmd, void *conf); 68 | #define NGX_HTTP_AUTH_DIGEST_BUF_SIZE 4096 69 | 70 | // digest challenge generation 71 | static ngx_int_t ngx_http_auth_digest_send_challenge(ngx_http_request_t *r, 72 | ngx_str_t *realm, 73 | ngx_uint_t is_stale); 74 | 75 | // digest response validators 76 | static ngx_int_t 77 | ngx_http_auth_digest_check_credentials(ngx_http_request_t *r, 78 | ngx_http_auth_digest_cred_t *ctx); 79 | static ngx_int_t 80 | ngx_http_auth_digest_verify_user(ngx_http_request_t *r, 81 | ngx_http_auth_digest_cred_t *fields, 82 | ngx_str_t *line); 83 | static ngx_int_t 84 | ngx_http_auth_digest_verify_hash(ngx_http_request_t *r, 85 | ngx_http_auth_digest_cred_t *fields, 86 | u_char *hashed_pw); 87 | 88 | // the shm segment that houses the used-nonces tree and evasion rbtree 89 | static ngx_uint_t ngx_http_auth_digest_shm_size; 90 | static ngx_shm_zone_t *ngx_http_auth_digest_shm_zone; 91 | static ngx_rbtree_t *ngx_http_auth_digest_rbtree; 92 | static ngx_rbtree_t *ngx_http_auth_digest_ev_rbtree; 93 | static char *ngx_http_auth_digest_set_shm_size(ngx_conf_t *cf, 94 | ngx_command_t *cmd, void *conf); 95 | static ngx_int_t ngx_http_auth_digest_init_shm_zone(ngx_shm_zone_t *shm_zone, 96 | void *data); 97 | 98 | // nonce bookkeeping 99 | static ngx_http_auth_digest_nonce_t 100 | ngx_http_auth_digest_next_nonce(ngx_http_request_t *r); 101 | static ngx_rbtree_node_t * 102 | ngx_http_auth_digest_rbtree_find(ngx_rbtree_key_t key, ngx_rbtree_node_t *node, 103 | ngx_rbtree_node_t *sentinel); 104 | 105 | // nonce cleanup 106 | #define NGX_HTTP_AUTH_DIGEST_CLEANUP_INTERVAL 3000 107 | #define NGX_HTTP_AUTH_DIGEST_CLEANUP_BATCH_SIZE 2048 108 | ngx_event_t *ngx_http_auth_digest_cleanup_timer; 109 | static ngx_array_t *ngx_http_auth_digest_cleanup_list; 110 | static ngx_atomic_t *ngx_http_auth_digest_cleanup_lock; 111 | void ngx_http_auth_digest_cleanup(ngx_event_t *e); 112 | static void ngx_http_auth_digest_rbtree_prune(ngx_log_t *log); 113 | static void ngx_http_auth_digest_rbtree_prune_walk(ngx_rbtree_node_t *node, 114 | ngx_rbtree_node_t *sentinel, 115 | time_t now, ngx_log_t *log); 116 | static void ngx_http_auth_digest_ev_rbtree_prune(ngx_log_t *log); 117 | static void 118 | ngx_http_auth_digest_ev_rbtree_prune_walk(ngx_rbtree_node_t *node, 119 | ngx_rbtree_node_t *sentinel, 120 | time_t now, ngx_log_t *log); 121 | 122 | // evasive tactics functions 123 | static int ngx_http_auth_digest_srcaddr_key(struct sockaddr *sa, socklen_t len, 124 | ngx_uint_t *key); 125 | static int ngx_http_auth_digest_srcaddr_cmp(struct sockaddr *sa1, 126 | socklen_t len1, 127 | struct sockaddr *sa2, 128 | socklen_t len2); 129 | static ngx_http_auth_digest_ev_node_t * 130 | ngx_http_auth_digest_ev_rbtree_find(ngx_http_auth_digest_ev_node_t *this, 131 | ngx_rbtree_node_t *node, 132 | ngx_rbtree_node_t *sentinel); 133 | #define NGX_HTTP_AUTH_DIGEST_STATUS_SUCCESS 1 134 | #define NGX_HTTP_AUTH_DIGEST_STATUS_FAILURE 0 135 | static void 136 | ngx_http_auth_digest_evasion_tracking(ngx_http_request_t *r, 137 | ngx_http_auth_digest_loc_conf_t *alcf, 138 | ngx_int_t status); 139 | static int ngx_http_auth_digest_evading(ngx_http_request_t *r, 140 | ngx_http_auth_digest_loc_conf_t *alcf); 141 | 142 | // rbtree primitives 143 | static void ngx_http_auth_digest_rbtree_insert(ngx_rbtree_node_t *temp, 144 | ngx_rbtree_node_t *node, 145 | ngx_rbtree_node_t *sentinel); 146 | static void ngx_http_auth_digest_ev_rbtree_insert(ngx_rbtree_node_t *temp, 147 | ngx_rbtree_node_t *node, 148 | ngx_rbtree_node_t *sentinel); 149 | static void 150 | ngx_rbtree_generic_insert(ngx_rbtree_node_t *temp, ngx_rbtree_node_t *node, 151 | ngx_rbtree_node_t *sentinel, 152 | int (*compare)(const ngx_rbtree_node_t *left, 153 | const ngx_rbtree_node_t *right)); 154 | static int ngx_http_auth_digest_rbtree_cmp(const ngx_rbtree_node_t *v_left, 155 | const ngx_rbtree_node_t *v_right); 156 | 157 | // quick & dirty bitvectors (for marking used nc values) 158 | static ngx_inline ngx_uint_t ngx_bitvector_size(ngx_uint_t nbits) { 159 | return ((nbits + CHAR_BIT - 1) / CHAR_BIT); 160 | } 161 | static ngx_inline ngx_uint_t ngx_bitvector_test(char *bv, ngx_uint_t bit) { 162 | return ((bv)[((bit) / CHAR_BIT)] & (1 << ((bit) % CHAR_BIT))); 163 | } 164 | static ngx_inline void ngx_bitvector_set(char *bv, ngx_uint_t bit) { 165 | ((bv)[((bit) / CHAR_BIT)] &= ~(1 << ((bit) % CHAR_BIT))); 166 | } 167 | 168 | // module plumbing 169 | static void *ngx_http_auth_digest_create_loc_conf(ngx_conf_t *cf); 170 | static char *ngx_http_auth_digest_merge_loc_conf(ngx_conf_t *cf, void *parent, 171 | void *child); 172 | static ngx_int_t ngx_http_auth_digest_init(ngx_conf_t *cf); 173 | static ngx_int_t ngx_http_auth_digest_worker_init(ngx_cycle_t *cycle); 174 | 175 | // module datastructures 176 | static ngx_command_t ngx_http_auth_digest_commands[] = { 177 | {ngx_string("auth_digest"), 178 | NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | 179 | NGX_HTTP_LMT_CONF | NGX_CONF_TAKE1, 180 | ngx_http_auth_digest_set_realm, NGX_HTTP_LOC_CONF_OFFSET, 181 | offsetof(ngx_http_auth_digest_loc_conf_t, realm), NULL}, 182 | {ngx_string("auth_digest_user_file"), 183 | NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | 184 | NGX_HTTP_LMT_CONF | NGX_CONF_TAKE1, 185 | ngx_http_auth_digest_set_user_file, NGX_HTTP_LOC_CONF_OFFSET, 186 | offsetof(ngx_http_auth_digest_loc_conf_t, user_file), NULL}, 187 | {ngx_string("auth_digest_timeout"), NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | 188 | NGX_HTTP_LOC_CONF | NGX_CONF_TAKE1, 189 | ngx_conf_set_sec_slot, NGX_HTTP_LOC_CONF_OFFSET, 190 | offsetof(ngx_http_auth_digest_loc_conf_t, timeout), NULL}, 191 | {ngx_string("auth_digest_expires"), NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | 192 | NGX_HTTP_LOC_CONF | NGX_CONF_TAKE1, 193 | ngx_conf_set_sec_slot, NGX_HTTP_LOC_CONF_OFFSET, 194 | offsetof(ngx_http_auth_digest_loc_conf_t, expires), NULL}, 195 | {ngx_string("auth_digest_drop_time"), 196 | NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | 197 | NGX_CONF_TAKE1, 198 | ngx_conf_set_sec_slot, NGX_HTTP_LOC_CONF_OFFSET, 199 | offsetof(ngx_http_auth_digest_loc_conf_t, drop_time), NULL}, 200 | {ngx_string("auth_digest_evasion_time"), 201 | NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | 202 | NGX_CONF_TAKE1, 203 | ngx_conf_set_sec_slot, NGX_HTTP_LOC_CONF_OFFSET, 204 | offsetof(ngx_http_auth_digest_loc_conf_t, evasion_time), NULL}, 205 | {ngx_string("auth_digest_replays"), NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | 206 | NGX_HTTP_LOC_CONF | NGX_CONF_TAKE1, 207 | ngx_conf_set_num_slot, NGX_HTTP_LOC_CONF_OFFSET, 208 | offsetof(ngx_http_auth_digest_loc_conf_t, replays), NULL}, 209 | {ngx_string("auth_digest_maxtries"), 210 | NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | 211 | NGX_CONF_TAKE1, 212 | ngx_conf_set_num_slot, NGX_HTTP_LOC_CONF_OFFSET, 213 | offsetof(ngx_http_auth_digest_loc_conf_t, maxtries), NULL}, 214 | {ngx_string("auth_digest_shm_size"), 215 | NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_CONF_TAKE1, 216 | ngx_http_auth_digest_set_shm_size, 0, 0, NULL}, 217 | ngx_null_command}; 218 | 219 | static ngx_http_module_t ngx_http_auth_digest_module_ctx = { 220 | NULL, /* preconfiguration */ 221 | ngx_http_auth_digest_init, /* postconfiguration */ 222 | 223 | NULL, /* create main configuration */ 224 | NULL, /* init main configuration */ 225 | 226 | NULL, /* create server configuration */ 227 | NULL, /* merge server configuration */ 228 | 229 | ngx_http_auth_digest_create_loc_conf, /* create location configuration */ 230 | ngx_http_auth_digest_merge_loc_conf /* merge location configuration */ 231 | }; 232 | 233 | ngx_module_t ngx_http_auth_digest_module = { 234 | NGX_MODULE_V1, 235 | &ngx_http_auth_digest_module_ctx, /* module context */ 236 | ngx_http_auth_digest_commands, /* module directives */ 237 | NGX_HTTP_MODULE, /* module type */ 238 | NULL, /* init master */ 239 | NULL, /* init module */ 240 | ngx_http_auth_digest_worker_init, /* init process */ 241 | NULL, /* init thread */ 242 | NULL, /* exit thread */ 243 | NULL, /* exit process */ 244 | NULL, /* exit master */ 245 | NGX_MODULE_V1_PADDING}; 246 | 247 | #endif 248 | -------------------------------------------------------------------------------- /readme.rst: -------------------------------------------------------------------------------- 1 | ================================== 2 | Nginx Digest Authentication module 3 | ================================== 4 | 5 | Changes from other forks 6 | ======================== 7 | Bug fixes 8 | `1 `_, 9 | `2 `_, 10 | `3 `_ 11 | 12 | `Added log message for invalid login attempts `_ 13 | 14 | Description 15 | =========== 16 | The ``ngx_http_auth_digest`` module supplements Nginx_'s built-in Basic Authentication `module`_ by providing support for `RFC`_ 2617 `Digest Authentication`_. The module is currently functional but has only been tested and reviewed by its author. And given that this is security code, one set of eyes is almost certainly insufficient to guarantee that it's 100% correct. Until a few bug reports come in and some of the ‘unknown unknowns’ in the code are flushed out, consider this module an ‘alpha’ and treat it with the appropriate amount of skepticism. 17 | 18 | A listing of known issues with the module can be found in the ``bugs.txt`` file as well as in the `Issue Tracker`_. Please do consider contributing a patch if you have the time and inclination. Any help fixing the bugs or changing the implementation to a more idiomatically nginx-y one would be greatly appreciated. 19 | 20 | Dependencies 21 | ============ 22 | * Sources for Nginx_ 1.0.x, and its dependencies. 23 | 24 | 25 | Building 26 | ======== 27 | 28 | 1. Unpack the Nginx_ sources:: 29 | 30 | $ tar zxvf nginx-1.0.x.tar.gz 31 | 32 | 2. Unpack the sources for the digest module:: 33 | 34 | $ tar xzvf samizdatco-nginx-http-auth-digest-xxxxxxx.tar.gz 35 | 36 | 3. Change to the directory which contains the Nginx_ sources, run the 37 | configuration script with the desired options and be sure to put an 38 | ``--add-module`` flag pointing to the directory which contains the source 39 | of the digest module:: 40 | 41 | $ cd nginx-1.0.x 42 | $ ./configure --add-module=../samizdatco-nginx-http-auth-digest-xxxxxxx [other configure options] 43 | 44 | 4. Build and install the software:: 45 | 46 | $ make && sudo make install 47 | 48 | 5. Configure Nginx_ using the module's configuration directives_. 49 | 50 | 51 | Example 52 | ======= 53 | 54 | You can password-protect a directory tree by adding the following lines into 55 | a ``server`` section in your Nginx_ configuration file:: 56 | 57 | auth_digest_user_file /opt/httpd/conf/passwd.digest; # a file created with htdigest 58 | location /private{ 59 | auth_digest 'this is not for you'; # set the realm for this location block 60 | } 61 | 62 | 63 | The other directives control the lifespan defaults for the authentication session. The 64 | following is equivalent to the previous example but demonstrates all the directives:: 65 | 66 | auth_digest_user_file /opt/httpd/conf/passwd.digest; 67 | auth_digest_shm_size 4m; # the storage space allocated for tracking active sessions 68 | 69 | location /private { 70 | auth_digest 'this is not for you'; 71 | auth_digest_timeout 60s; # allow users to wait 1 minute between receiving the 72 | # challenge and hitting send in the browser dialog box 73 | auth_digest_expires 10s; # after a successful challenge/response, let the client 74 | # continue to use the same nonce for additional requests 75 | # for 10 seconds before generating a new challenge 76 | auth_digest_replays 20; # also generate a new challenge if the client uses the 77 | # same nonce more than 20 times before the expire time limit 78 | } 79 | 80 | Adding digest authentication to a location will affect any uris that match that block. To 81 | disable authentication for specific sub-branches off a uri, set ``auth_digest`` to ``off``:: 82 | 83 | location / { 84 | auth_digest 'this is not for you'; 85 | location /pub { 86 | auth_digest off; # this sub-tree will be accessible without authentication 87 | } 88 | } 89 | 90 | Directives 91 | ========== 92 | 93 | auth_digest 94 | ~~~~~~~~~~~ 95 | :Syntax: ``auth_digest`` [*realm-name* | ``off``] 96 | :Default: ``off`` 97 | :Context: server, location 98 | :Description: 99 | Enable or disable digest authentication for a server or location block. The realm name 100 | should correspond to a realm used in the user file. Any user within that realm will be 101 | able to access files after authenticating. 102 | 103 | To selectively disable authentication within a protected uri hierarchy, set ``auth_digest`` 104 | to “``off``” within a more-specific location block (see example). 105 | 106 | 107 | auth_digest_user_file 108 | ~~~~~~~~~~~~~~~~~~~~~ 109 | :Syntax: ``auth_digest_user_file`` */path/to/passwd/file* 110 | :Default: *unset* 111 | :Context: server, location 112 | :Description: 113 | The password file should be of the form created by the apache ``htdigest`` command (or the 114 | included `htdigest.py`_ script). Each line of the file is a colon-separated list composed 115 | of a username, realm, and md5 hash combining name, realm, and password. For example: 116 | ``joi:enfield:ef25e85b34208c246cfd09ab76b01db7`` 117 | This file needs to be readable by your nginx user! 118 | 119 | auth_digest_timeout 120 | ~~~~~~~~~~~~~~~~~~~ 121 | :Syntax: ``auth_digest_timeout`` *delay-time* 122 | :Default: ``60s`` 123 | :Context: server, location 124 | :Description: 125 | When a client first requests a protected page, the server returns a 401 status code along with 126 | a challenge in the ``www-authenticate`` header. 127 | 128 | At this point most browsers will present a dialog box to the user prompting them to log in. This 129 | directive defines how long challenges will remain valid. If the user waits longer than this time 130 | before submitting their name and password, the challenge will be considered ‘stale’ and they will 131 | be prompted to log in again. 132 | 133 | auth_digest_expires 134 | ~~~~~~~~~~~~~~~~~~~ 135 | :Syntax: ``auth_digest_expires`` *lifetime-in-seconds* 136 | :Default: ``10s`` 137 | :Context: server, location 138 | :Description: 139 | Once a digest challenge has been successfully answered by the client, subsequent requests 140 | will attempt to re-use the ‘nonce’ value from the original challenge. To complicate MitM_ 141 | attacks, it's best to limit the number of times a cached nonce will be accepted. This 142 | directive sets the duration for this re-use period after the first successful authentication. 143 | 144 | auth_digest_replays 145 | ~~~~~~~~~~~~~~~~~~~ 146 | :Syntax: ``auth_digest_replays`` *number-of-uses* 147 | :Default: ``20`` 148 | :Context: server, location 149 | :Description: 150 | Nonce re-use should also be limited to a fixed number of requests. Note that increasing this 151 | value will cause a proportional increase in memory usage and the shm_size may have to be 152 | adjusted to keep up with heavy traffic within the digest-protected location blocks. 153 | 154 | auth_digest_evasion_time 155 | ~~~~~~~~~~~~~~~~~~~~~~~~ 156 | :Syntax: ``auth_digest_evasion_time`` *time-in-seconds* 157 | :Default: ``300s`` 158 | :Context: server, location 159 | :Description: 160 | The amount of time for which the server will ignore authentication requests from a client 161 | address once the number of failed authentications from that client reaches ``auth_digest_maxtries``. 162 | 163 | auth_digest_maxtries 164 | ~~~~~~~~~~~~~~~~~~~~ 165 | :Syntax: ``auth_digest_maxtries`` *number-of-attempts* 166 | :Default: ``5`` 167 | :Context: server, location 168 | :Description: 169 | The number of failed authentication attempts from a client address before the module enters 170 | evasive tactics. For evasion purposes, only network clients are tracked, and only by address 171 | (not including port number). A successful authentication clears the counters. 172 | 173 | auth_digest_shm_size 174 | ~~~~~~~~~~~~~~~~~~~~ 175 | :Syntax: ``auth_digest_shm_size`` *size-in-bytes* 176 | :Default: ``4096k`` 177 | :Context: server 178 | :Description: 179 | The module maintains a fixed-size cache of active digest sessions to save state between 180 | authenticated requests. Once this cache is full, no further authentication will be possible 181 | until active sessions expire. 182 | 183 | As a result, choosing the proper size is a little tricky since it depends upon the values set in 184 | the expiration-related directives. Each stored challenge takes up ``48 + ceil(replays/8)`` bytes 185 | and will live for up to ``auth_digest_timeout + auth_digest_expires`` seconds. When using the 186 | default module settings this translates into allowing around 82k non-replay requests every 70 187 | seconds. 188 | 189 | .. _nginx: http://nginx.net 190 | .. _module: http://wiki.nginx.org/HttpAuthBasicModule 191 | .. _htdigest.py: https://github.com/samizdatco/nginx-http-auth-digest/blob/master/htdigest.py 192 | .. _RFC: http://www.ietf.org/rfc/rfc2617.txt 193 | .. _Digest Authentication: http://en.wikipedia.org/wiki/Digest_access_authentication 194 | .. _Issue Tracker: https://github.com/samizdatco/nginx-http-auth-digest/issues 195 | .. _MitM: http://en.wikipedia.org/wiki/Man-in-the-middle_attack 196 | --------------------------------------------------------------------------------