[mod_auth] "nonce_secret" option to validate nonce (fixes #2976)
"nonce_secret" option to validate nonce was generated by the server Marginally hardens HTTP Digest Auth. Necessary piece, but not sufficient, to restrict re-use of nonce (mitigations for replay or limiting nonce count reuse via nc=... are not implemented) x-ref: "Digest auth nonces are not validated" https://redmine.lighttpd.net/issues/2976
This commit is contained in:
parent
9fdf24468d
commit
e1188e770e
|
@ -29,6 +29,7 @@ struct http_auth_backend_t;
|
|||
typedef struct http_auth_require_t {
|
||||
const struct http_auth_scheme_t *scheme;
|
||||
const buffer *realm;
|
||||
const buffer *nonce_secret;
|
||||
int valid_user;
|
||||
int algorithm;
|
||||
array user;
|
||||
|
|
185
src/mod_auth.c
185
src/mod_auth.c
|
@ -263,6 +263,7 @@ static handler_t mod_auth_require_parse_array(const array *value, array * const
|
|||
size_t m;
|
||||
data_array *da_file = (data_array *)value->data[n];
|
||||
const buffer *method = NULL, *realm = NULL, *require = NULL;
|
||||
const buffer *nonce_secret = NULL;
|
||||
const http_auth_scheme_t *auth_scheme;
|
||||
buffer *algos = NULL;
|
||||
int algorithm = HTTP_AUTH_DIGEST_SESS;
|
||||
|
@ -286,6 +287,8 @@ static handler_t mod_auth_require_parse_array(const array *value, array * const
|
|||
require = &ds->value;
|
||||
} else if (buffer_is_equal_string(&ds->key, CONST_STR_LEN("algorithm"))) {
|
||||
algos = &ds->value;
|
||||
} else if (buffer_is_equal_string(&ds->key, CONST_STR_LEN("nonce_secret"))) {
|
||||
nonce_secret = &ds->value;
|
||||
} else {
|
||||
log_error(errh, __FILE__, __LINE__,
|
||||
"the field is unknown in: "
|
||||
|
@ -348,6 +351,7 @@ static handler_t mod_auth_require_parse_array(const array *value, array * const
|
|||
dauth->require->scheme = auth_scheme;
|
||||
dauth->require->algorithm = algorithm;
|
||||
dauth->require->realm = realm;
|
||||
dauth->require->nonce_secret = nonce_secret; /*(NULL is ok)*/
|
||||
if (!mod_auth_require_parse(dauth->require, require, errh)) {
|
||||
dauth->fn->free((data_unset *)dauth);
|
||||
return HANDLER_ERROR;
|
||||
|
@ -662,7 +666,7 @@ static void mod_auth_digest_mutate_sha256(http_auth_info_t *ai, const char *m, c
|
|||
SHA256_Final(ai->digest, &ctx);
|
||||
}
|
||||
|
||||
static void mod_auth_digest_nonce_sha256(buffer *b, time_t cur_ts, int rnd) {
|
||||
static void mod_auth_digest_nonce_sha256(buffer *b, time_t cur_ts, int rnd, const buffer *secret) {
|
||||
SHA256_CTX ctx;
|
||||
size_t len;
|
||||
unsigned char h[HTTP_AUTH_DIGEST_SHA256_BINLEN];
|
||||
|
@ -673,6 +677,10 @@ static void mod_auth_digest_nonce_sha256(buffer *b, time_t cur_ts, int rnd) {
|
|||
SHA256_Update(&ctx, (unsigned char *)hh, len);
|
||||
len = li_itostrn(hh, sizeof(hh), rnd);
|
||||
SHA256_Update(&ctx, (unsigned char *)hh, len);
|
||||
if (secret) {
|
||||
len = buffer_string_length(secret);
|
||||
SHA256_Update(&ctx, (unsigned char *)secret->ptr, len);
|
||||
}
|
||||
SHA256_Final(h, &ctx);
|
||||
li_tohex(hh, sizeof(hh), (const char *)h, sizeof(h));
|
||||
buffer_append_string_len(b, hh, sizeof(hh)-1);
|
||||
|
@ -731,7 +739,7 @@ static void mod_auth_digest_mutate_sha512_256(http_auth_info_t *ai, const char *
|
|||
SHA512_256_Final(ai->digest, &ctx);
|
||||
}
|
||||
|
||||
static void mod_auth_digest_nonce_sha512_256(buffer *b, time_t cur_ts, int rnd) {
|
||||
static void mod_auth_digest_nonce_sha512_256(buffer *b, time_t cur_ts, int rnd, const buffer *secret) {
|
||||
SHA512_CTX ctx;
|
||||
size_t len;
|
||||
unsigned char h[HTTP_AUTH_DIGEST_SHA512_256_BINLEN];
|
||||
|
@ -742,6 +750,10 @@ static void mod_auth_digest_nonce_sha512_256(buffer *b, time_t cur_ts, int rnd)
|
|||
SHA512_256_Update(&ctx, (unsigned char *)hh, len);
|
||||
len = li_itostrn(hh, sizeof(hh), rnd);
|
||||
SHA512_256_Update(&ctx, (unsigned char *)hh, len);
|
||||
if (secret) {
|
||||
len = buffer_string_length(secret);
|
||||
SHA512_256_Update(&ctx, (unsigned char *)secret->ptr, len);
|
||||
}
|
||||
SHA512_256_Final(h, &ctx);
|
||||
li_tohex(hh, sizeof(hh), (const char *)h, sizeof(h));
|
||||
buffer_append_string_len(b, hh, sizeof(hh)-1);
|
||||
|
@ -804,7 +816,7 @@ static void mod_auth_digest_mutate_md5(http_auth_info_t *ai, const char *m, cons
|
|||
li_MD5_Final(ai->digest, &ctx);
|
||||
}
|
||||
|
||||
static void mod_auth_digest_nonce_md5(buffer *b, time_t cur_ts, int rnd) {
|
||||
static void mod_auth_digest_nonce_md5(buffer *b, time_t cur_ts, int rnd, const buffer *secret) {
|
||||
li_MD5_CTX ctx;
|
||||
size_t len;
|
||||
unsigned char h[HTTP_AUTH_DIGEST_MD5_BINLEN];
|
||||
|
@ -815,6 +827,10 @@ static void mod_auth_digest_nonce_md5(buffer *b, time_t cur_ts, int rnd) {
|
|||
li_MD5_Update(&ctx, (unsigned char *)hh, len);
|
||||
len = li_itostrn(hh, sizeof(hh), rnd);
|
||||
li_MD5_Update(&ctx, (unsigned char *)hh, len);
|
||||
if (secret) {
|
||||
len = buffer_string_length(secret);
|
||||
li_MD5_Update(&ctx, (unsigned char *)secret->ptr, len);
|
||||
}
|
||||
li_MD5_Final(h, &ctx);
|
||||
li_tohex(hh, sizeof(hh), (const char *)h, sizeof(h));
|
||||
buffer_append_string_len(b, hh, sizeof(hh)-1);
|
||||
|
@ -833,31 +849,62 @@ static void mod_auth_digest_mutate(http_auth_info_t *ai, const char *m, const ch
|
|||
#endif
|
||||
}
|
||||
|
||||
static void mod_auth_append_nonce(buffer *b, time_t cur_ts, const struct http_auth_require_t *require, int dalgo, int *rndptr) {
|
||||
buffer_append_uint_hex(b, (uintmax_t)cur_ts);
|
||||
buffer_append_string_len(b, CONST_STR_LEN(":"));
|
||||
const buffer * const nonce_secret = require->nonce_secret;
|
||||
int rnd;
|
||||
if (NULL == nonce_secret)
|
||||
rnd = rndptr ? *rndptr : li_rand_pseudo();
|
||||
else { /*(do not directly expose random number generator single value)*/
|
||||
rndptr
|
||||
? (void)(rnd = *rndptr)
|
||||
: li_rand_pseudo_bytes((unsigned char *)&rnd, sizeof(rnd));
|
||||
buffer_append_uint_hex(b, (uintmax_t)rnd);
|
||||
buffer_append_string_len(b, CONST_STR_LEN(":"));
|
||||
}
|
||||
switch (dalgo) {
|
||||
#ifdef USE_OPENSSL_CRYPTO
|
||||
#ifdef SHA512_256_DIGEST_LENGTH
|
||||
case HTTP_AUTH_DIGEST_SHA512_256:
|
||||
mod_auth_digest_nonce_sha512_256(b, cur_ts, rnd, nonce_secret);
|
||||
break;
|
||||
#endif
|
||||
case HTTP_AUTH_DIGEST_SHA256:
|
||||
mod_auth_digest_nonce_sha256(b, cur_ts, rnd, nonce_secret);
|
||||
break;
|
||||
#endif
|
||||
/*case HTTP_AUTH_DIGEST_MD5:*/
|
||||
default:
|
||||
mod_auth_digest_nonce_md5(b, cur_ts, rnd, nonce_secret);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void mod_auth_digest_www_authenticate(buffer *b, time_t cur_ts, const struct http_auth_require_t *require, int nonce_stale) {
|
||||
const int rnd = li_rand_pseudo();
|
||||
int algos = nonce_stale ? nonce_stale : require->algorithm;
|
||||
int n = 0;
|
||||
void(*append_nonce[3])(buffer *, time_t, int);
|
||||
int algoid[3];
|
||||
unsigned int algolen[3];
|
||||
const char *algoname[3];
|
||||
#ifdef USE_OPENSSL_CRYPTO
|
||||
#ifdef SHA512_256_DIGEST_LENGTH
|
||||
if (algos & HTTP_AUTH_DIGEST_SHA512_256) {
|
||||
append_nonce[n] = mod_auth_digest_nonce_sha512_256;
|
||||
algoid[n] = HTTP_AUTH_DIGEST_SHA512_256;
|
||||
algoname[n] = "SHA-512-256";
|
||||
algolen[n] = sizeof("SHA-512-256")-1;
|
||||
++n;
|
||||
}
|
||||
#endif
|
||||
if (algos & HTTP_AUTH_DIGEST_SHA256) {
|
||||
append_nonce[n] = mod_auth_digest_nonce_sha256;
|
||||
algoid[n] = HTTP_AUTH_DIGEST_SHA256;
|
||||
algoname[n] = "SHA-256";
|
||||
algolen[n] = sizeof("SHA-256")-1;
|
||||
++n;
|
||||
}
|
||||
#endif
|
||||
if (algos & HTTP_AUTH_DIGEST_MD5) {
|
||||
append_nonce[n] = mod_auth_digest_nonce_md5;
|
||||
algoid[n] = HTTP_AUTH_DIGEST_MD5;
|
||||
algoname[n] = "MD5";
|
||||
algolen[n] = sizeof("MD5")-1;
|
||||
++n;
|
||||
|
@ -873,9 +920,7 @@ static void mod_auth_digest_www_authenticate(buffer *b, time_t cur_ts, const str
|
|||
buffer_append_string_len(b, CONST_STR_LEN("\", charset=\"UTF-8\", algorithm="));
|
||||
buffer_append_string_len(b, algoname[i], algolen[i]);
|
||||
buffer_append_string_len(b, CONST_STR_LEN(", nonce=\""));
|
||||
buffer_append_uint_hex(b, (uintmax_t)cur_ts);
|
||||
buffer_append_string_len(b, CONST_STR_LEN(":"));
|
||||
(append_nonce[i])(b, cur_ts, rnd);
|
||||
mod_auth_append_nonce(b, cur_ts, require, algoid[i], NULL);
|
||||
buffer_append_string_len(b, CONST_STR_LEN("\", qop=\"auth\""));
|
||||
if (nonce_stale) {
|
||||
buffer_append_string_len(b, CONST_STR_LEN(", stale=true"));
|
||||
|
@ -883,30 +928,10 @@ static void mod_auth_digest_www_authenticate(buffer *b, time_t cur_ts, const str
|
|||
}
|
||||
}
|
||||
|
||||
static void mod_auth_digest_authentication_info(buffer *b, time_t cur_ts, int dalgo) {
|
||||
const int rnd = li_rand_pseudo();
|
||||
void(*append_nonce)(buffer *, time_t, int);
|
||||
switch (dalgo) {
|
||||
#ifdef USE_OPENSSL_CRYPTO
|
||||
#ifdef SHA512_256_DIGEST_LENGTH
|
||||
case HTTP_AUTH_DIGEST_SHA512_256:
|
||||
append_nonce = mod_auth_digest_nonce_sha512_256;
|
||||
break;
|
||||
#endif
|
||||
case HTTP_AUTH_DIGEST_SHA256:
|
||||
append_nonce = mod_auth_digest_nonce_sha256;
|
||||
break;
|
||||
#endif
|
||||
/*case HTTP_AUTH_DIGEST_MD5:*/
|
||||
default:
|
||||
append_nonce = mod_auth_digest_nonce_md5;
|
||||
break;
|
||||
}
|
||||
static void mod_auth_digest_authentication_info(buffer *b, time_t cur_ts, const struct http_auth_require_t *require, int dalgo) {
|
||||
buffer_clear(b);
|
||||
buffer_append_string_len(b, CONST_STR_LEN("nextnonce=\""));
|
||||
buffer_append_uint_hex(b, (uintmax_t)cur_ts);
|
||||
buffer_append_string_len(b, CONST_STR_LEN(":"));
|
||||
(append_nonce)(b, cur_ts, rnd);
|
||||
mod_auth_append_nonce(b, cur_ts, require, dalgo, NULL);
|
||||
buffer_append_string_len(b, CONST_STR_LEN("\""));
|
||||
}
|
||||
|
||||
|
@ -932,7 +957,6 @@ static handler_t mod_auth_check_digest(request_st * const r, void *p_d, const st
|
|||
char *respons = NULL;
|
||||
|
||||
char *e, *c;
|
||||
const char *m = NULL;
|
||||
int i;
|
||||
buffer *b;
|
||||
http_auth_info_t ai;
|
||||
|
@ -1093,9 +1117,6 @@ static handler_t mod_auth_check_digest(request_st * const r, void *p_d, const st
|
|||
return mod_auth_send_400_bad_request(r);
|
||||
}
|
||||
|
||||
m = get_http_method_name(r->http_method);
|
||||
force_assert(m);
|
||||
|
||||
/* detect if attacker is attempting to reuse valid digest for one uri
|
||||
* on a different request uri. Might also happen if intermediate proxy
|
||||
* altered client request line. (Altered request would not result in
|
||||
|
@ -1115,6 +1136,65 @@ static handler_t mod_auth_check_digest(request_st * const r, void *p_d, const st
|
|||
}
|
||||
}
|
||||
|
||||
/* check age of nonce. Note, random data is used in nonce generation
|
||||
* in mod_auth_send_401_unauthorized_digest(). If that were replaced
|
||||
* with nanosecond time, then nonce secret would remain unique enough
|
||||
* for the purposes of Digest auth, and would be reproducible (and
|
||||
* verifiable) if nanoseconds were included with seconds as part of the
|
||||
* nonce "timestamp:secret". However, doing so would expose a high
|
||||
* precision timestamp of the system to attackers. timestamp in nonce
|
||||
* could theoretically be modified and still produce same md5sum, but
|
||||
* that is highly unlikely within a 10 min (moving) window of valid
|
||||
* time relative to current time (now).
|
||||
* If it is desired to validate that nonces were generated by server,
|
||||
* then specify auth.require = ( ... => ( "secret" => "..." ) )
|
||||
* When secret is specified, then instead of nanoseconds, the random
|
||||
* data value (included for unique nonces) will be exposed in the nonce
|
||||
* along with the timestamp, and the additional secret will be used to
|
||||
* validate that the server generated the nonce using that secret. */
|
||||
int send_nextnonce;
|
||||
{
|
||||
time_t ts = 0;
|
||||
const unsigned char * const nonce_uns = (unsigned char *)nonce;
|
||||
for (i = 0; i < 8 && light_isxdigit(nonce_uns[i]); ++i) {
|
||||
ts = (ts << 4) + hex2int(nonce_uns[i]);
|
||||
}
|
||||
const time_t cur_ts = log_epoch_secs;
|
||||
if (nonce[i] != ':'
|
||||
|| ts > cur_ts || cur_ts - ts > 600) { /*(10 mins)*/
|
||||
/* nonce is stale; have client regenerate digest */
|
||||
buffer_free(b);
|
||||
return mod_auth_send_401_unauthorized_digest(r, require, ai.dalgo);
|
||||
}
|
||||
|
||||
send_nextnonce = (cur_ts - ts > 540); /*(9 mins)*/
|
||||
|
||||
if (require->nonce_secret) {
|
||||
unsigned int rnd = 0;
|
||||
for (int j = i+8; i < j && light_isxdigit(nonce_uns[i]); ++i) {
|
||||
rnd = (rnd << 4) + hex2int(nonce_uns[i]);
|
||||
}
|
||||
if (nonce[i] != ':') {
|
||||
/* nonce is invalid;
|
||||
* expect extra field w/ require->nonce_secret */
|
||||
log_error(r->conf.errh, __FILE__, __LINE__,
|
||||
"digest: nonce invalid");
|
||||
buffer_free(b);
|
||||
return mod_auth_send_400_bad_request(r);
|
||||
}
|
||||
buffer * const tb = r->tmp_buf;
|
||||
buffer_clear(tb);
|
||||
mod_auth_append_nonce(tb, cur_ts, require, ai.dalgo, (int *)&rnd);
|
||||
if (!buffer_eq_slen(tb, nonce, strlen(nonce))) {
|
||||
/* nonce not generated using current require->nonce_secret */
|
||||
log_error(r->conf.errh, __FILE__, __LINE__,
|
||||
"digest: nonce mismatch");
|
||||
buffer_free(b);
|
||||
return mod_auth_send_401_unauthorized_digest(r, require, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch (backend->digest(r, backend->p_d, &ai)) {
|
||||
case HANDLER_GO_ON:
|
||||
break;
|
||||
|
@ -1131,6 +1211,9 @@ static handler_t mod_auth_check_digest(request_st * const r, void *p_d, const st
|
|||
return mod_auth_send_401_unauthorized_digest(r, require, 0);
|
||||
}
|
||||
|
||||
const char *m = get_http_method_name(r->http_method);
|
||||
force_assert(m);
|
||||
|
||||
mod_auth_digest_mutate(&ai,m,uri,nonce,cnonce,nc,qop);
|
||||
|
||||
if (!http_auth_const_time_memeq(rdigest, ai.digest, ai.dlen)) {
|
||||
|
@ -1150,36 +1233,14 @@ static handler_t mod_auth_check_digest(request_st * const r, void *p_d, const st
|
|||
return mod_auth_send_401_unauthorized_digest(r, require, 0);
|
||||
}
|
||||
|
||||
/* check age of nonce. Note, random data is used in nonce generation
|
||||
* in mod_auth_send_401_unauthorized_digest(). If that were replaced
|
||||
* with nanosecond time, then nonce secret would remain unique enough
|
||||
* for the purposes of Digest auth, and would be reproducible (and
|
||||
* verifiable) if nanoseconds were inclued with seconds as part of the
|
||||
* nonce "timestamp:secret". Since that is not done, timestamp in
|
||||
* nonce could theoretically be modified and still produce same md5sum,
|
||||
* but that is highly unlikely within a 10 min (moving) window of valid
|
||||
* time relative to current time (now) */
|
||||
{
|
||||
time_t ts = 0;
|
||||
const unsigned char * const nonce_uns = (unsigned char *)nonce;
|
||||
for (i = 0; i < 8 && light_isxdigit(nonce_uns[i]); ++i) {
|
||||
ts = (ts << 4) + hex2int(nonce_uns[i]);
|
||||
}
|
||||
const time_t cur_ts = log_epoch_secs;
|
||||
if (nonce[i] != ':'
|
||||
|| ts > cur_ts || cur_ts - ts > 600) { /*(10 mins)*/
|
||||
/* nonce is stale; have client regenerate digest */
|
||||
buffer_free(b);
|
||||
return mod_auth_send_401_unauthorized_digest(r, require, ai.dalgo);
|
||||
}
|
||||
else if (cur_ts - ts > 540) { /*(9 mins)*/
|
||||
if (send_nextnonce) {
|
||||
/*(send nextnonce when expiration is approaching)*/
|
||||
buffer * const tb = r->tmp_buf;
|
||||
mod_auth_digest_authentication_info(tb, cur_ts, ai.dalgo);
|
||||
const time_t cur_ts = log_epoch_secs;
|
||||
mod_auth_digest_authentication_info(tb, cur_ts, require, ai.dalgo);
|
||||
http_header_response_set(r, HTTP_HEADER_OTHER,
|
||||
CONST_STR_LEN("Authentication-Info"),
|
||||
CONST_BUF_LEN(tb));
|
||||
}
|
||||
}
|
||||
|
||||
http_auth_setenv(r, ai.username, ai.ulen, CONST_STR_LEN("Digest"));
|
||||
|
|
Loading…
Reference in New Issue