| /* Licensed to the Apache Software Foundation (ASF) under one or more | |
| * contributor license agreements. See the NOTICE file distributed with | |
| * this work for additional information regarding copyright ownership. | |
| * The ASF licenses this file to You under the Apache License, Version 2.0 | |
| * (the "License"); you may not use this file except in compliance with | |
| * the License. You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, software | |
| * distributed under the License is distributed on an "AS IS" BASIS, | |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| * See the License for the specific language governing permissions and | |
| * limitations under the License. | |
| */ | |
| /* | |
| * mod_auth_digest: MD5 digest authentication | |
| * | |
| * Originally by Alexei Kosut <akosut@nueva.pvt.k12.ca.us> | |
| * Updated to RFC-2617 by Ronald Tschalär <ronald@innovation.ch> | |
| * based on mod_auth, by Rob McCool and Robert S. Thau | |
| * | |
| * Open Issues: | |
| * - MD5-sess and auth-int are not implemented; auth-int needs stream and | |
| * trailer support. An incomplete implementation has been removed and | |
| * can be retrieved from svn history. | |
| * - Proxy-Authentication-Info header is set by this module, but is | |
| * currently ignored by mod_proxy (needs patch to mod_proxy) | |
| * - The source of the secret should be run-time directive (with server | |
| * scope: RSRC_CONF). Each server generates its own, so a nonce issued | |
| * by one does not verify at another, and a client moving between them | |
| * is re-challenged every time. | |
| * - Sharing a realm among multiple servers needs more than that secret, | |
| * though. It would be enough for a configuration which tracks no | |
| * per-client state, but the client table, the ids which key it and the | |
| * one-time nonce counter are all per-server, so under AuthDigestNcCheck | |
| * or AuthDigestNonceLifetime 0 a client would still be unknown to | |
| * whichever server it reached next. | |
| * - Sharing the secret would also make a request captured against one | |
| * server replayable against the others, and the nonce-count check | |
| * would not stop that, since each server counts separately. Hashing | |
| * the server name and port into the nonce would, but that is the | |
| * opposite of sharing: the two would have to be configured explicitly. | |
| */ | |
| #include "apu_version.h" | |
| #include "apr_sha1.h" | |
| #include "apr_base64.h" | |
| #include "apr_lib.h" | |
| #include "apr_time.h" | |
| #include "apr_errno.h" | |
| #include "apr_global_mutex.h" | |
| #include "apr_strings.h" | |
| #include "apr_atomic.h" | |
| #define APR_WANT_STRFUNC | |
| #include "apr_want.h" | |
| #include "ap_config.h" | |
| #include "httpd.h" | |
| #include "http_config.h" | |
| #include "http_core.h" | |
| #include "http_request.h" | |
| #include "http_log.h" | |
| #include "http_protocol.h" | |
| #include "apr_uri.h" | |
| #include "util_md5.h" | |
| #include "util_mutex.h" | |
| #include "apr_shm.h" | |
| #include "apr_rmm.h" | |
| #include "ap_provider.h" | |
| #include "mod_auth.h" | |
| #if APR_HAVE_UNISTD_H | |
| #include <unistd.h> | |
| #endif | |
| /* configure declines to build this module without both of these, so this | |
| * only catches builds which don't use it. */ | |
| #if !APR_HAS_RANDOM || !APR_HAS_SHARED_MEMORY | |
| #error mod_auth_digest requires APR with random and shared memory support | |
| #endif | |
| /* The nonce is authenticated with a keyed hash, so that a client cannot | |
| * forge one. apr_siphash() is a MAC, and is what that calls for; it is | |
| * available in APU 1.6 / APR 2.0 and later. Older APR falls back to the | |
| * original SHA-1 of the secret followed by the message, which is not a | |
| * sound construction - a Merkle-Damgard hash keyed by a prefix can be | |
| * extended without the key - but which is not attackable here, since the | |
| * padding such an extension needs cannot survive the opaque being parsed | |
| * as a hex number before the nonce is checked. */ | |
| #if APU_MAJOR_VERSION > 1 || (APU_MAJOR_VERSION == 1 && APU_MINOR_VERSION >= 6) | |
| #define DIGEST_SIPHASH_NONCE 1 | |
| #include "apr_siphash.h" | |
| #else | |
| #define DIGEST_SIPHASH_NONCE 0 | |
| #endif | |
| /* struct to hold the configuration info */ | |
| typedef struct digest_config_struct { | |
| authn_provider_list *providers; | |
| apr_time_t nonce_lifetime; | |
| int check_nc; | |
| const char *algorithm; /* currently a constant (MD5). */ | |
| char *uri_list; | |
| } digest_config_rec; | |
| /* Whether state is tracked per client under conf: a one-time nonce is | |
| * retired by its use, and AuthDigestNcCheck needs the last nonce-count. A | |
| * client is issued an opaque iff so, since the opaque is the key to that | |
| * state. */ | |
| static int tracks_clients(const digest_config_rec *conf) | |
| { | |
| return conf->check_nc || conf->nonce_lifetime == 0; | |
| } | |
| #define DFLT_ALGORITHM "MD5" | |
| #define DFLT_NONCE_LIFE apr_time_from_sec(300) | |
| #define NEXTNONCE_DELTA apr_time_from_sec(30) | |
| /* The server nonce has fixed length and is the concatenation of: | |
| * base64(apr_time_t timestamp) + hex(keyed hash of realm+time[+opaque]) | |
| * The hash is half as long with siphash, which produces 64 bits: enough for | |
| * a value which can only be attacked by presenting it to the server, there | |
| * being no way to test a candidate offline. */ | |
| #define NONCE_TIME_LEN (((sizeof(apr_time_t)+2)/3)*4) | |
| #if DIGEST_SIPHASH_NONCE | |
| #define NONCE_HASH_LEN (2*APR_SIPHASH_DSIZE) | |
| #else | |
| #define NONCE_HASH_LEN (2*APR_SHA1_DIGESTSIZE) | |
| #endif | |
| #define NONCE_LEN (int )(NONCE_TIME_LEN + NONCE_HASH_LEN) | |
| /* Evaluates to true if nonce string is valid. Since the time part of | |
| * the nonce is a base64 encoding of an apr_time_t (8 bytes), it | |
| * must end with a '='. */ | |
| #define VALID_NONCE(n_) ((n_) && strlen((n_)) == NONCE_LEN && (n_)[NONCE_TIME_LEN - 1] == '=') | |
| #define MD5_DIGEST_LEN (2*APR_MD5_DIGESTSIZE) /* ignoring trailing \0 */ | |
| #define SECRET_LEN 20 | |
| #define RETAINED_DATA_ID "mod_auth_digest" | |
| #if DIGEST_SIPHASH_NONCE && SECRET_LEN < APR_SIPHASH_KSIZE | |
| #error the secret is too short to key siphash | |
| #endif | |
| /* Identifies a client entry. This is the value sent to the client in the | |
| * opaque field of the challenge, and echoed back in its Authorization | |
| * header; zero is never a valid id, and means "no client". Ids are counted | |
| * out by client_generate() from client_list->next_id, and are kept within | |
| * 1..CLIENT_ID_MAX so that the opaque can be parsed by apr_strtoi64(). */ | |
| typedef apr_uint64_t client_id_t; | |
| #define CLIENT_ID_MAX APR_INT64_MAX | |
| typedef struct hash_entry { | |
| client_id_t key; /* the key for this entry */ | |
| struct hash_entry *next; /* next entry in the bucket */ | |
| unsigned long nonce_count; /* highest nonce-count seen | |
| * for last_nonce_time */ | |
| apr_time_t last_nonce_time; /* nonce of the last request | |
| * accepted for this client */ | |
| apr_uint64_t owner; /* owner_hash() of the user | |
| * who first used this entry, | |
| * zero until then */ | |
| } client_entry; | |
| /* The client table, in the shared memory segment. Once initialize_tables() | |
| * has set it up, the table, the entries reachable from it and every field | |
| * below may only be read or modified with client_lock held. */ | |
| static struct hash_table { | |
| client_entry **table; | |
| unsigned long tbl_len; | |
| unsigned long num_entries; | |
| unsigned long num_created; | |
| unsigned long num_removed; | |
| unsigned long num_renewed; | |
| client_id_t next_id; /* the last id issued (the random | |
| * initial seed is never issued) */ | |
| apr_uint64_t otn_counter; /* the last one-time nonce ordinal */ | |
| } *client_list; | |
| /* Outcome from parsing an Authorization header. */ | |
| enum hdr_sts { NO_HEADER, NOT_DIGEST, INVALID, VALID }; | |
| /* Outcome of checking a request's nonce and nonce-count against the state | |
| * tracked for its client. */ | |
| enum nonce_state { | |
| NONCE_ACCEPTED, /* recorded as the latest used by this client */ | |
| NONCE_UNKNOWN, /* the client has no entry (garbage collected?) */ | |
| NONCE_STALE, /* one-time nonce already used */ | |
| NONCE_BAD_COUNT, /* nonce-count did not increase: possible replay */ | |
| NONCE_NOT_OWNER /* the entry quoted belongs to another user */ | |
| }; | |
| /* struct to hold a parsed Authorization header */ | |
| typedef struct digest_header_struct { | |
| const char *scheme; | |
| const char *realm; | |
| const char *username; | |
| char *nonce; | |
| const char *uri; | |
| const char *method; | |
| const char *digest; | |
| const char *algorithm; | |
| const char *cnonce; | |
| const char *opaque; | |
| client_id_t opaque_num; | |
| const char *message_qop; | |
| const char *nonce_count; | |
| unsigned long nc; /* nonce_count parsed, when valid */ | |
| /* the following fields are not (directly) from the header */ | |
| const char *raw_request_uri; | |
| apr_uri_t *psd_request_uri; | |
| apr_time_t nonce_time; | |
| enum hdr_sts auth_hdr_sts; | |
| int needed_auth; | |
| const char *ha1; | |
| } digest_header_rec; | |
| typedef union time_union { | |
| apr_time_t time; | |
| unsigned char arr[sizeof(apr_time_t)]; | |
| } time_rec; | |
| static unsigned char *secret; | |
| static apr_shm_t *client_shm = NULL; | |
| static apr_rmm_t *client_rmm = NULL; | |
| static apr_global_mutex_t *client_lock = NULL; | |
| static const char *client_mutex_type = "authdigest-client"; | |
| static const char *client_shm_filename; | |
| #define DEF_SHMEM_SIZE 8192L /* ~ 120 entries */ | |
| #define HASH_DEPTH 5 | |
| /* Buckets for a given segment size, so that the default and | |
| * AuthDigestShmemSize cannot disagree. */ | |
| #define NUM_BUCKETS(size_) (((size_) - sizeof(*client_list)) / \ | |
| (sizeof(client_entry *) \ | |
| + HASH_DEPTH * sizeof(client_entry))) | |
| #define DEF_NUM_BUCKETS NUM_BUCKETS(DEF_SHMEM_SIZE) | |
| static apr_size_t shmem_size = DEF_SHMEM_SIZE; | |
| static unsigned long num_buckets = DEF_NUM_BUCKETS; | |
| module AP_MODULE_DECLARE_DATA auth_digest_module; | |
| /* Logs a failure to set up the client table, returning the hook result. */ | |
| static int init_failed(server_rec *s, apr_status_t rv, const char *msg) | |
| { | |
| ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, APLOGNO(01760) | |
| "%s - cannot track Digest clients", msg); | |
| return !OK; | |
| } | |
| /* RMM helper functions that behave like single-step malloc/free. */ | |
| static void *rmm_malloc(apr_rmm_t *rmm, apr_size_t size) | |
| { | |
| apr_rmm_off_t offset = apr_rmm_malloc(rmm, size); | |
| if (!offset) { | |
| return NULL; | |
| } | |
| return apr_rmm_addr_get(rmm, offset); | |
| } | |
| static apr_status_t rmm_free(apr_rmm_t *rmm, void *alloc) | |
| { | |
| apr_rmm_off_t offset = apr_rmm_offset_get(rmm, alloc); | |
| return apr_rmm_free(rmm, offset); | |
| } | |
| static int initialize_tables(server_rec *s, apr_pool_t *ctx) | |
| { | |
| unsigned long idx; | |
| apr_status_t sts; | |
| client_shm = NULL; | |
| client_rmm = NULL; | |
| client_lock = NULL; | |
| client_list = NULL; | |
| /* | |
| * Create a unique filename using our pid. This information is | |
| * stashed in the global variable so the children inherit it. | |
| */ | |
| client_shm_filename = ap_runtime_dir_relative(ctx, "authdigest_shm"); | |
| client_shm_filename = ap_append_pid(ctx, client_shm_filename, "."); | |
| /* Use anonymous shm by default, fall back on name-based. */ | |
| sts = apr_shm_create(&client_shm, shmem_size, NULL, ctx); | |
| if (APR_STATUS_IS_ENOTIMPL(sts)) { | |
| /* For a name-based segment, remove it first in case of a | |
| * previous unclean shutdown. */ | |
| apr_shm_remove(client_shm_filename, ctx); | |
| /* Now create that segment */ | |
| sts = apr_shm_create(&client_shm, shmem_size, | |
| client_shm_filename, ctx); | |
| } | |
| if (APR_SUCCESS != sts) { | |
| return init_failed(s, sts, apr_psprintf(ctx, "failed to create " | |
| "shared memory segment on " | |
| "file %s", | |
| client_shm_filename)); | |
| } | |
| sts = apr_rmm_init(&client_rmm, | |
| NULL, /* no lock, we'll do the locking ourselves */ | |
| apr_shm_baseaddr_get(client_shm), | |
| shmem_size, ctx); | |
| if (sts != APR_SUCCESS) { | |
| return init_failed(s, sts, "failed to initialize rmm"); | |
| } | |
| client_list = rmm_malloc(client_rmm, sizeof(*client_list) + | |
| sizeof(client_entry *) * num_buckets); | |
| if (!client_list) { | |
| return init_failed(s, 0, "failed to allocate shared memory"); | |
| } | |
| client_list->table = (client_entry**) (client_list + 1); | |
| for (idx = 0; idx < num_buckets; idx++) { | |
| client_list->table[idx] = NULL; | |
| } | |
| client_list->tbl_len = num_buckets; | |
| client_list->num_entries = 0; | |
| /* Start the ids at a random point rather than at 1. This segment does | |
| * not survive a restart, but the nonces naming its entries do, since the | |
| * secret they are hashed with is retained; ids restarting from 1 too | |
| * would hand a returning client's id straight back out, so that client | |
| * would be checked against whichever new client now held it. The ids are | |
| * not secret - they are sent in the clear as the opaque - but over a | |
| * range of 2^63 neither wrapping the counter nor walking it from a | |
| * random start to an id issued before the restart is feasible. */ | |
| ap_random_insecure_bytes(&client_list->next_id, | |
| sizeof client_list->next_id); | |
| client_list->next_id %= CLIENT_ID_MAX; | |
| client_list->otn_counter = 0; | |
| sts = ap_global_mutex_create(&client_lock, NULL, client_mutex_type, NULL, | |
| s, ctx, 0); | |
| if (sts != APR_SUCCESS) { | |
| return init_failed(s, sts, "failed to create lock (client_lock)"); | |
| } | |
| return OK; | |
| } | |
| static int pre_init(apr_pool_t *pconf, apr_pool_t *plog, apr_pool_t *ptemp) | |
| { | |
| apr_status_t rv; | |
| void *retained; | |
| rv = ap_mutex_register(pconf, client_mutex_type, NULL, APR_LOCK_DEFAULT, 0); | |
| if (rv != APR_SUCCESS) | |
| return !OK; | |
| retained = ap_retained_data_get(RETAINED_DATA_ID); | |
| if (retained == NULL) { | |
| retained = ap_retained_data_create(RETAINED_DATA_ID, SECRET_LEN); | |
| ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, NULL, APLOGNO(01757) | |
| "generating secret for digest authentication"); | |
| rv = apr_generate_random_bytes(retained, SECRET_LEN); | |
| if (rv != APR_SUCCESS) { | |
| ap_log_error(APLOG_MARK, APLOG_CRIT, rv, NULL, APLOGNO(01758) | |
| "error generating secret"); | |
| return !OK; | |
| } | |
| } | |
| secret = retained; | |
| return OK; | |
| } | |
| static int initialize_module(apr_pool_t *p, apr_pool_t *plog, | |
| apr_pool_t *ptemp, server_rec *s) | |
| { | |
| /* initialize_module() will be called twice, and if it's a DSO | |
| * then all static data from the first call will be lost. Only | |
| * set up our static data on the second call. */ | |
| if (ap_state_query(AP_SQ_MAIN_STATE) == AP_SQ_MS_CREATE_PRE_CONFIG) | |
| return OK; | |
| /* The client table belongs to one configuration generation: it is | |
| * allocated from pconf, which the restart loop clears - destroying the | |
| * segment - before running this hook again to create a new one. A | |
| * child of the previous generation keeps the mapping it inherited | |
| * until it exits, so the tables are not pulled out from under it. | |
| * | |
| * Per-client state therefore does not survive a restart, and neither | |
| * does the client id space; see the seeding in initialize_tables(). | |
| * A client which returns afterwards is simply unknown, and is | |
| * re-challenged with stale=true at the cost of one extra request. | |
| */ | |
| return initialize_tables(s, p); | |
| } | |
| /* A failure here is logged and the child carries on with the handles it | |
| * inherited: nothing is torn down, since the shared memory and above all | |
| * the mutex are shared with every other child, and this child has no | |
| * fallback for serving Digest requests without them. */ | |
| static void initialize_child(apr_pool_t *p, server_rec *s) | |
| { | |
| apr_status_t sts; | |
| /* Get access to rmm in child */ | |
| sts = apr_rmm_attach(&client_rmm, | |
| NULL, | |
| apr_shm_baseaddr_get(client_shm), | |
| p); | |
| if (sts != APR_SUCCESS) { | |
| ap_log_error(APLOG_MARK, APLOG_CRIT, sts, s, APLOGNO(10676) | |
| "failed to attach to rmm in child"); | |
| return; | |
| } | |
| sts = apr_global_mutex_child_init(&client_lock, | |
| apr_global_mutex_lockfile(client_lock), | |
| p); | |
| if (sts != APR_SUCCESS) { | |
| ap_log_error(APLOG_MARK, APLOG_CRIT, sts, s, APLOGNO(10677) | |
| "failed to initialize lock (client_lock) in child"); | |
| } | |
| } | |
| /* Configuration handling. */ | |
| static void *create_digest_dir_config(apr_pool_t *p, char *dir) | |
| { | |
| digest_config_rec *conf = apr_pcalloc(p, sizeof *conf); | |
| conf->nonce_lifetime = DFLT_NONCE_LIFE; | |
| conf->algorithm = DFLT_ALGORITHM; | |
| return conf; | |
| } | |
| static const char *add_authn_provider(cmd_parms *cmd, void *config, | |
| const char *arg) | |
| { | |
| digest_config_rec *conf = (digest_config_rec*)config; | |
| authn_provider_list *newp; | |
| newp = apr_pcalloc(cmd->pool, sizeof(authn_provider_list)); | |
| newp->provider_name = arg; | |
| /* lookup and cache the actual provider now */ | |
| newp->provider = ap_lookup_provider(AUTHN_PROVIDER_GROUP, | |
| newp->provider_name, | |
| AUTHN_PROVIDER_VERSION); | |
| if (newp->provider == NULL) { | |
| /* by the time they use it, the provider should be loaded and | |
| registered with us. */ | |
| return apr_psprintf(cmd->pool, | |
| "Unknown Authn provider: %s", | |
| newp->provider_name); | |
| } | |
| if (!newp->provider->get_realm_hash) { | |
| /* if it doesn't provide the appropriate function, reject it */ | |
| return apr_psprintf(cmd->pool, | |
| "The '%s' Authn provider doesn't support " | |
| "Digest Authentication", newp->provider_name); | |
| } | |
| /* Add it to the list now. */ | |
| if (!conf->providers) { | |
| conf->providers = newp; | |
| } | |
| else { | |
| authn_provider_list *last = conf->providers; | |
| while (last->next) { | |
| last = last->next; | |
| } | |
| last->next = newp; | |
| } | |
| return NULL; | |
| } | |
| static const char *set_qop(cmd_parms *cmd, void *config, const char *op) | |
| { | |
| if (ap_cstr_casecmp(op, "auth")) { | |
| return "AuthDigestQop is deprecated: only 'auth' is supported"; | |
| } | |
| return NULL; | |
| } | |
| static const char *set_nonce_lifetime(cmd_parms *cmd, void *config, | |
| const char *t) | |
| { | |
| char *endptr; | |
| long lifetime; | |
| lifetime = strtol(t, &endptr, 10); | |
| if (endptr < (t+strlen(t)) && !apr_isspace(*endptr)) { | |
| return apr_pstrcat(cmd->pool, | |
| "Invalid time in AuthDigestNonceLifetime: ", | |
| t, NULL); | |
| } | |
| ((digest_config_rec *) config)->nonce_lifetime = apr_time_from_sec(lifetime); | |
| return NULL; | |
| } | |
| static const char *set_nc_check(cmd_parms *cmd, void *config, int flag) | |
| { | |
| ((digest_config_rec *) config)->check_nc = flag; | |
| return NULL; | |
| } | |
| static const char *set_algorithm(cmd_parms *cmd, void *config, const char *alg) | |
| { | |
| if (ap_cstr_casecmp(alg, "MD5")) { | |
| return apr_pstrcat(cmd->pool, "Unsupported algorithm in AuthDigestAlgorithm: ", alg, NULL); | |
| } | |
| /* conf->algorithm remains the constant, "MD5". */ | |
| return NULL; | |
| } | |
| static const char *set_uri_list(cmd_parms *cmd, void *config, const char *uri) | |
| { | |
| digest_config_rec *c = (digest_config_rec *) config; | |
| if (c->uri_list) { | |
| c->uri_list[strlen(c->uri_list)-1] = '\0'; | |
| c->uri_list = apr_pstrcat(cmd->pool, c->uri_list, " ", uri, "\"", NULL); | |
| } | |
| else { | |
| c->uri_list = apr_pstrcat(cmd->pool, ", domain=\"", uri, "\"", NULL); | |
| } | |
| return NULL; | |
| } | |
| static const char *set_shmem_size(cmd_parms *cmd, void *config, | |
| const char *size_str) | |
| { | |
| apr_off_t size; | |
| apr_size_t min; | |
| apr_status_t rv; | |
| rv = ap_size_parameter_parse(size_str, &size); | |
| if (rv == APR_ERANGE) { | |
| return "AuthDigestShmemSize value is too large"; | |
| } | |
| if (rv != APR_SUCCESS || size < 0) { | |
| return apr_pstrcat(cmd->pool, "Invalid size in AuthDigestShmemSize: ", | |
| size_str, NULL); | |
| } | |
| /* The segment must hold two separate rmm allocations -- the client | |
| * list with at least one bucket, and at least one client entry -- each | |
| * with its rmm overhead. */ | |
| min = apr_rmm_overhead_get(2) | |
| + sizeof(*client_list) + sizeof(client_entry *) | |
| + sizeof(client_entry); | |
| if (size < (apr_off_t)min) { | |
| return apr_psprintf(cmd->pool, "size in AuthDigestShmemSize too small: " | |
| "%" APR_OFF_T_FMT " < %" APR_SIZE_T_FMT, size, min); | |
| } | |
| shmem_size = (apr_size_t)size; | |
| num_buckets = NUM_BUCKETS(shmem_size); | |
| if (num_buckets == 0) { | |
| num_buckets = 1; | |
| } | |
| ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, cmd->server, APLOGNO(01763) | |
| "Set shmem-size: %" APR_SIZE_T_FMT ", num-buckets: %ld", | |
| shmem_size, num_buckets); | |
| return NULL; | |
| } | |
| static const command_rec digest_cmds[] = | |
| { | |
| AP_INIT_ITERATE("AuthDigestProvider", add_authn_provider, NULL, OR_AUTHCFG, | |
| "specify the auth providers for a directory or location"), | |
| AP_INIT_ITERATE("AuthDigestQop", set_qop, NULL, OR_AUTHCFG, | |
| "A list of quality-of-protection options"), | |
| AP_INIT_TAKE1("AuthDigestNonceLifetime", set_nonce_lifetime, NULL, OR_AUTHCFG, | |
| "Maximum lifetime of the server nonce (seconds)"), | |
| AP_INIT_FLAG("AuthDigestNcCheck", set_nc_check, NULL, OR_AUTHCFG, | |
| "Whether or not to check the nonce-count sent by the client"), | |
| AP_INIT_TAKE1("AuthDigestAlgorithm", set_algorithm, NULL, OR_AUTHCFG, | |
| "The algorithm used for the hash calculation"), | |
| AP_INIT_ITERATE("AuthDigestDomain", set_uri_list, NULL, OR_AUTHCFG, | |
| "A list of URI's which belong to the same protection space as the current URI"), | |
| AP_INIT_TAKE1("AuthDigestShmemSize", set_shmem_size, NULL, RSRC_CONF, | |
| "The amount of shared memory to allocate for keeping track of clients"), | |
| {NULL} | |
| }; | |
| /* The client list. | |
| * | |
| * Each client is assigned a number, which is transferred in the opaque | |
| * field of the WWW-Authenticate and Authorization headers. The number | |
| * is just a simple counter which is incremented for each new client. | |
| * Clients can't forge this number because it is hashed up into the | |
| * server nonce, and that is checked. | |
| * | |
| * The clients are kept in a simple hash table, which consists of an | |
| * array of client_entry's, each with a linked list of entries hanging | |
| * off it. The client's number modulo the size of the array gives the | |
| * bucket number. | |
| * | |
| * The clients are garbage collected whenever a new client is allocated | |
| * but there is not enough space left in the shared memory segment. A | |
| * simple semi-LRU is used for this: whenever a client entry is accessed | |
| * it is moved to the beginning of the linked list in its bucket (this | |
| * also makes for faster lookups for current clients). The garbage | |
| * collecter then just removes the oldest entry (i.e. the one at the | |
| * end of the list) in each bucket. | |
| * | |
| * The main advantages of the above scheme are that it's easy to implement | |
| * and it keeps the hash table evenly balanced (i.e. same number of entries | |
| * in each bucket). The major disadvantage is that you may be throwing | |
| * entries out which are in active use. This is not tragic, as these | |
| * clients will just be sent a new client id (opaque field) and nonce | |
| * with a stale=true (i.e. it will just look like the nonce expired, | |
| * thereby forcing an extra round trip). If the shared memory segment | |
| * has enough headroom over the current client set size then this should | |
| * not occur too often. | |
| * | |
| * To help tune the size of the shared memory segment (and see if the | |
| * above algorithm is really sufficient) a set of counters is kept | |
| * indicating the number of clients held, the number of garbage collected | |
| * clients, and the number of erroneously purged clients. These are printed | |
| * out at each garbage collection run. Note that no attempt is made to | |
| * guarantee that num_renewed is correct in the face of clients spoofing | |
| * the opaque field; it is just an indicator, and whether it is off by a | |
| * few doesn't matter. | |
| */ | |
| /* | |
| * Find the client given its client number (the key), moving it to the | |
| * front of its bucket. Returns the entry, or NULL if it's not found. | |
| * | |
| * MUST be called with client_lock held, and the entry returned MUST NOT be | |
| * used outside that critical section: it lives in the shared memory | |
| * segment, where gc() can free it at any time on behalf of another | |
| * process. The accessors below are the only supported way to reach a | |
| * client entry; each looks it up afresh, so a client which has since been | |
| * garbage collected is simply reported as unknown and the caller goes on | |
| * to issue a new challenge for it. | |
| * | |
| * Note that this still gives no ordering guarantee for a client using | |
| * multiple simultaneous connections within the same protection space: the | |
| * requests can be processed in any order, so the nonce-count and one-time | |
| * nonce checks may reject some of them. That is not new. | |
| */ | |
| static client_entry *find_client(client_id_t key) | |
| { | |
| int bucket; | |
| client_entry *entry, *prev = NULL; | |
| if (!key) { | |
| return NULL; | |
| } | |
| bucket = key % client_list->tbl_len; | |
| entry = client_list->table[bucket]; | |
| while (entry && key != entry->key) { | |
| prev = entry; | |
| entry = entry->next; | |
| } | |
| if (entry && prev) { /* move entry to front of list */ | |
| prev->next = entry->next; | |
| entry->next = client_list->table[bucket]; | |
| client_list->table[bucket] = entry; | |
| } | |
| return entry; | |
| } | |
| /* Determine whether the client identified by key is still known. */ | |
| static int client_exists(client_id_t key, const request_rec *r) | |
| { | |
| int found; | |
| apr_global_mutex_lock(client_lock); | |
| found = find_client(key) != NULL; | |
| apr_global_mutex_unlock(client_lock); | |
| if (found) { | |
| ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(01764) | |
| "client %" APR_UINT64_T_FMT " found", key); | |
| } | |
| else { | |
| ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(01765) | |
| "client %" APR_UINT64_T_FMT " not found", key); | |
| } | |
| return found; | |
| } | |
| /* Identifies the authenticated user of r, within the origin server and | |
| * realm, for binding a client entry to. A keyed hash, so that it is | |
| * fixed-size in shared memory and a username colliding with another's | |
| * cannot be found. Never zero. | |
| * | |
| * AuthDigestDomain is deliberately not part of this: a client keeps one | |
| * nonce and opaque per server and realm whatever domains it was given, so | |
| * sections of one realm which differ in their domain would make every | |
| * crossing between them a stale challenge and a fresh entry. */ | |
| static apr_uint64_t owner_hash(request_rec *r) | |
| { | |
| const char *realm = ap_auth_name(r); | |
| const char *msg = apr_psprintf(r->pool, "%s:%" APR_SIZE_T_FMT ":%s:%s", | |
| r->server->server_hostname, | |
| (apr_size_t)strlen(realm), realm, r->user); | |
| apr_uint64_t owner; | |
| #if DIGEST_SIPHASH_NONCE | |
| owner = apr_siphash24(msg, strlen(msg), secret); | |
| #else | |
| unsigned char sha1[APR_SHA1_DIGESTSIZE]; | |
| apr_sha1_ctx_t ctx; | |
| apr_sha1_init(&ctx); | |
| apr_sha1_update_binary(&ctx, secret, SECRET_LEN); | |
| apr_sha1_update_binary(&ctx, (const unsigned char *) msg, strlen(msg)); | |
| apr_sha1_final(sha1, &ctx); | |
| memcpy(&owner, sha1, sizeof(owner)); | |
| #endif | |
| /* zero is reserved for an entry nobody has used yet */ | |
| return owner ? owner : 1; | |
| } | |
| /* Check the nonce generated at nonce_time, and the nonce-count nc sent | |
| * with it, against the state tracked for the client identified by key, and | |
| * record them if acceptable. | |
| * | |
| * Both nonce_time and the count are compared against what the client last | |
| * *used*, never against what was last issued to it: a nonce is issued | |
| * whenever a challenge is generated, and anything quoting the client's | |
| * opaque can provoke a challenge, so tracking what was issued lets an | |
| * unauthenticated request invalidate the nonce which the legitimate client | |
| * is holding. | |
| * | |
| * A one-time nonce (AuthDigestNonceLifetime 0) is therefore accepted iff | |
| * it is newer than the last nonce this client used, which permits it | |
| * exactly once. Otherwise, with AuthDigestNcCheck, a newer nonce starts a | |
| * new count and the same nonce must raise it. | |
| * | |
| * Must only be called for a request which is fully verified - both the | |
| * response digest and the nonce - so that a request which fails to | |
| * authenticate cannot alter the state tracked for the client whose opaque | |
| * it quotes. | |
| * | |
| * The opaque is public, so a verified request from anybody with an account | |
| * on the realm could otherwise do so too. An entry is bound to the user | |
| * who first used it, and a request from another user quoting it is | |
| * NONCE_NOT_OWNER, leaving the entry alone. | |
| * | |
| * The nonce-count last used by the client is returned in *tracked_nc (zero | |
| * if the client is unknown or not this user's), for the caller to log. */ | |
| static enum nonce_state client_update_nonce(request_rec *r, | |
| client_id_t key, | |
| const digest_config_rec *conf, | |
| apr_time_t nonce_time, | |
| unsigned long nc, | |
| unsigned long *tracked_nc) | |
| { | |
| client_entry *client; | |
| enum nonce_state state; | |
| apr_uint64_t owner = owner_hash(r); | |
| *tracked_nc = 0; | |
| apr_global_mutex_lock(client_lock); | |
| client = find_client(key); | |
| if (client == NULL) { | |
| state = NONCE_UNKNOWN; | |
| } | |
| else if (client->owner && client->owner != owner) { | |
| state = NONCE_NOT_OWNER; | |
| } | |
| else { | |
| *tracked_nc = client->nonce_count; | |
| if (conf->nonce_lifetime == 0) { | |
| /* one-time nonce: usable until it has been used */ | |
| state = (nonce_time > client->last_nonce_time) | |
| ? NONCE_ACCEPTED : NONCE_STALE; | |
| } | |
| else if (nonce_time > client->last_nonce_time | |
| || (nonce_time == client->last_nonce_time | |
| && nc > *tracked_nc)) { | |
| state = NONCE_ACCEPTED; | |
| } | |
| else { | |
| state = NONCE_BAD_COUNT; | |
| } | |
| if (state == NONCE_ACCEPTED) { | |
| client->last_nonce_time = nonce_time; | |
| client->nonce_count = nc; | |
| client->owner = owner; | |
| } | |
| } | |
| apr_global_mutex_unlock(client_lock); | |
| return state; | |
| } | |
| /* A simple garbage-collecter to remove unused clients. It removes the | |
| * last entry in each bucket and updates the counters, returning the | |
| * number of removed entries in *num_removed. The result is the first | |
| * failure to free an entry, if any, for the caller to log once the lock | |
| * is released. | |
| */ | |
| static apr_status_t gc(unsigned long *num_removed) | |
| { | |
| apr_status_t rv = APR_SUCCESS; | |
| unsigned long idx; | |
| *num_removed = 0; | |
| /* garbage collect one entry from each bucket */ | |
| for (idx = 0; idx < client_list->tbl_len; idx++) { | |
| client_entry **link, **victim = NULL; | |
| int unused = 0; | |
| /* The last entry is the least recently used, since find_client() | |
| * moves an entry to the front on each access; but prefer a client | |
| * which has never completed an authentication, whose entry records | |
| * nothing and so costs it nothing to lose. Every request which | |
| * fails to authenticate allocates one of those, and without this | |
| * they would evict the clients which are using the server. */ | |
| for (link = &client_list->table[idx]; *link; link = &(*link)->next) { | |
| if ((*link)->last_nonce_time == 0) { | |
| victim = link; | |
| unused = 1; | |
| } | |
| else if (!unused) { | |
| victim = link; | |
| } | |
| } | |
| if (victim) { | |
| client_entry *entry = *victim; | |
| apr_status_t err; | |
| *victim = entry->next; | |
| err = rmm_free(client_rmm, entry); | |
| (*num_removed)++; | |
| if (err && rv == APR_SUCCESS) { | |
| rv = err; | |
| } | |
| } | |
| } | |
| /* update counters */ | |
| client_list->num_entries -= *num_removed; | |
| client_list->num_removed += *num_removed; | |
| return rv; | |
| } | |
| /* | |
| * Add a new client to the list, under a newly issued id; renewed is set | |
| * when it replaces an entry which had been garbage collected. Returns the | |
| * id if successful, zero otherwise. This triggers the garbage collection | |
| * if memory is low. (The new entry is not returned: see find_client().) | |
| */ | |
| static client_id_t client_generate(request_rec *r, int renewed) | |
| { | |
| server_rec *s = r->server; | |
| int bucket; | |
| client_entry *entry; | |
| client_id_t key = 0; | |
| int ran_gc = 0; | |
| unsigned long gc_removed = 0, created, removed, renewals; | |
| apr_status_t gc_rv = APR_SUCCESS; | |
| apr_global_mutex_lock(client_lock); | |
| /* try to allocate a new entry */ | |
| entry = rmm_malloc(client_rmm, sizeof(client_entry)); | |
| if (!entry) { | |
| gc_rv = gc(&gc_removed); | |
| ran_gc = 1; | |
| entry = rmm_malloc(client_rmm, sizeof(client_entry)); | |
| } | |
| if (entry) { | |
| /* now issue the id, in 1..CLIENT_ID_MAX, and add the entry */ | |
| key = client_list->next_id = client_list->next_id % CLIENT_ID_MAX + 1; | |
| bucket = key % client_list->tbl_len; | |
| memset(entry, 0, sizeof(client_entry)); | |
| entry->key = key; | |
| entry->next = client_list->table[bucket]; | |
| client_list->table[bucket] = entry; | |
| client_list->num_created++; | |
| client_list->num_entries++; | |
| client_list->num_renewed += !!renewed; | |
| } | |
| /* for logging after the lock is released */ | |
| created = client_list->num_created; | |
| removed = client_list->num_removed; | |
| renewals = client_list->num_renewed; | |
| apr_global_mutex_unlock(client_lock); | |
| if (gc_rv) { | |
| /* Nothing we can really do but log... */ | |
| ap_log_error(APLOG_MARK, APLOG_ERR, gc_rv, s, APLOGNO(10007) | |
| "Failed to free auth_digest client allocation"); | |
| } | |
| if (ran_gc) { | |
| ap_log_error(APLOG_MARK, APLOG_INFO, 0, s, APLOGNO(01766) | |
| "gc'd %lu client entries. Total new clients: " | |
| "%lu; Total removed clients: %lu; Total renewed clients: " | |
| "%lu", gc_removed, created - renewals, removed, | |
| renewals); | |
| } | |
| if (!key) { | |
| ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01769) | |
| "unable to allocate a client entry - failing the " | |
| "request, since this configuration needs one"); | |
| return 0; | |
| } | |
| ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO(01768) | |
| "allocated new client %" APR_UINT64_T_FMT, key); | |
| return key; | |
| } | |
| /* Parse the Authorization header, if it exists, into resp; returns the | |
| * status of the header. */ | |
| static enum hdr_sts parse_digest_header(request_rec *r, | |
| digest_header_rec *resp) | |
| { | |
| const char *auth_line; | |
| apr_size_t l; | |
| int vk = 0, vv = 0; | |
| char *key, *value; | |
| auth_line = apr_table_get(r->headers_in, | |
| (PROXYREQ_PROXY == r->proxyreq) | |
| ? "Proxy-Authorization" | |
| : "Authorization"); | |
| if (!auth_line) { | |
| return NO_HEADER; | |
| } | |
| resp->scheme = ap_getword_white(r->pool, &auth_line); | |
| if (ap_cstr_casecmp(resp->scheme, "Digest")) { | |
| return NOT_DIGEST; | |
| } | |
| l = strlen(auth_line); | |
| key = apr_palloc(r->pool, l+1); | |
| value = apr_palloc(r->pool, l+1); | |
| while (auth_line[0] != '\0') { | |
| /* find key */ | |
| while (apr_isspace(auth_line[0])) { | |
| auth_line++; | |
| } | |
| vk = 0; | |
| while (auth_line[0] != '=' && auth_line[0] != ',' | |
| && auth_line[0] != '\0' && !apr_isspace(auth_line[0])) { | |
| key[vk++] = *auth_line++; | |
| } | |
| key[vk] = '\0'; | |
| while (apr_isspace(auth_line[0])) { | |
| auth_line++; | |
| } | |
| /* find value */ | |
| vv = 0; | |
| if (auth_line[0] == '=') { | |
| auth_line++; | |
| while (apr_isspace(auth_line[0])) { | |
| auth_line++; | |
| } | |
| if (auth_line[0] == '\"') { /* quoted string */ | |
| auth_line++; | |
| while (auth_line[0] != '\"' && auth_line[0] != '\0') { | |
| if (auth_line[0] == '\\' && auth_line[1] != '\0') { | |
| auth_line++; /* escaped char */ | |
| } | |
| value[vv++] = *auth_line++; | |
| } | |
| if (auth_line[0] != '\0') { | |
| auth_line++; | |
| } | |
| } | |
| else { /* token */ | |
| while (auth_line[0] != ',' && auth_line[0] != '\0' | |
| && !apr_isspace(auth_line[0])) { | |
| value[vv++] = *auth_line++; | |
| } | |
| } | |
| } | |
| value[vv] = '\0'; | |
| while (auth_line[0] != ',' && auth_line[0] != '\0') { | |
| auth_line++; | |
| } | |
| if (auth_line[0] != '\0') { | |
| auth_line++; | |
| } | |
| if (!ap_cstr_casecmp(key, "username")) | |
| resp->username = apr_pstrdup(r->pool, value); | |
| else if (!ap_cstr_casecmp(key, "realm")) | |
| resp->realm = apr_pstrdup(r->pool, value); | |
| else if (!ap_cstr_casecmp(key, "nonce")) | |
| resp->nonce = apr_pstrdup(r->pool, value); | |
| else if (!ap_cstr_casecmp(key, "uri")) | |
| resp->uri = apr_pstrdup(r->pool, value); | |
| else if (!ap_cstr_casecmp(key, "response")) | |
| resp->digest = apr_pstrdup(r->pool, value); | |
| else if (!ap_cstr_casecmp(key, "algorithm")) | |
| resp->algorithm = apr_pstrdup(r->pool, value); | |
| else if (!ap_cstr_casecmp(key, "cnonce")) | |
| resp->cnonce = apr_pstrdup(r->pool, value); | |
| else if (!ap_cstr_casecmp(key, "opaque")) | |
| resp->opaque = apr_pstrdup(r->pool, value); | |
| else if (!ap_cstr_casecmp(key, "qop")) | |
| resp->message_qop = apr_pstrdup(r->pool, value); | |
| else if (!ap_cstr_casecmp(key, "nc")) | |
| resp->nonce_count = apr_pstrdup(r->pool, value); | |
| } | |
| if (!resp->username || !resp->realm || !resp->uri | |
| || !VALID_NONCE(resp->nonce) | |
| || !resp->digest || strlen(resp->digest) != MD5_DIGEST_LEN | |
| || (resp->message_qop && (!resp->cnonce || !resp->nonce_count))) { | |
| return INVALID; | |
| } | |
| if (resp->opaque) { | |
| char *endptr; | |
| apr_int64_t num; | |
| num = apr_strtoi64(resp->opaque, &endptr, 16); | |
| if (errno || *endptr != '\0' || num <= 0 || num > CLIENT_ID_MAX) { | |
| return INVALID; | |
| } | |
| resp->opaque_num = (client_id_t)num; | |
| } | |
| /* The nonce-count, when present, is 8 hex digits (RFC 7616 3.4); check | |
| * it here so it is a well-formed count everywhere it is later used or | |
| * echoed, rather than parsed leniently into a wrapped or negative value | |
| * or reflected verbatim into the Authentication-Info header. */ | |
| if (resp->nonce_count != NULL) { | |
| int i; | |
| for (i = 0; i < 8 && apr_isxdigit(resp->nonce_count[i]); i++) { | |
| continue; | |
| } | |
| if (i != 8 || resp->nonce_count[8] != '\0') { | |
| return INVALID; | |
| } | |
| resp->nc = strtoul(resp->nonce_count, NULL, 16); | |
| } | |
| return VALID; | |
| } | |
| /* Create the per-request Digest record describing request r, store it on | |
| * store's request_config, and parse r's Authorization header into it. */ | |
| static digest_header_rec *make_digest_rec(request_rec *store, request_rec *r) | |
| { | |
| digest_header_rec *resp = apr_pcalloc(store->pool, sizeof(*resp)); | |
| resp->raw_request_uri = r->unparsed_uri; | |
| resp->psd_request_uri = &r->parsed_uri; | |
| resp->method = r->method; | |
| ap_set_module_config(store->request_config, &auth_digest_module, resp); | |
| resp->auth_hdr_sts = parse_digest_header(r, resp); | |
| return resp; | |
| } | |
| /* Set up the per-request record: this is the place to get the request-uri | |
| * (before any subrequests etc are initiated), to initialize the | |
| * request_config, and to parse the Authorization header. | |
| * | |
| * Note that the nonce-count tracked for the client is deliberately NOT | |
| * updated here: the state of an authenticated client must not be altered | |
| * by a request which has not (yet) been authenticated, or a replayed or | |
| * bogus request quoting the client's opaque would be able to rewind that | |
| * state. See check_nonce(). | |
| * | |
| * Note that this must be called after mod_proxy had its go so that | |
| * r->proxyreq is set correctly. | |
| */ | |
| static int init_digest_request(request_rec *r) | |
| { | |
| if (!ap_is_initial_req(r)) { | |
| return DECLINED; | |
| } | |
| make_digest_rec(r, r); | |
| return DECLINED; | |
| } | |
| /* Return the Digest record shared across this request tree, which lives on | |
| * the initial request. init_digest_request creates it in post_read_request; | |
| * a request which never ran that hook (one rejected before it and brought | |
| * here by an ErrorDocument or other internal redirect) has none, so create | |
| * it now rather than dereferencing NULL. */ | |
| static digest_header_rec *get_digest_rec(request_rec *r) | |
| { | |
| request_rec *mainreq = r; | |
| digest_header_rec *resp; | |
| while (mainreq->main != NULL) { | |
| mainreq = mainreq->main; | |
| } | |
| while (mainreq->prev != NULL) { | |
| mainreq = mainreq->prev; | |
| } | |
| resp = ap_get_module_config(mainreq->request_config, &auth_digest_module); | |
| if (resp == NULL) { | |
| resp = make_digest_rec(mainreq, r); | |
| } | |
| return resp; | |
| } | |
| /* Writes the hash part of the server nonce to hash, which must be of | |
| * minimum size (NONCE_HASH_LEN+1). */ | |
| static void gen_nonce_hash(apr_pool_t *p, char hash[NONCE_HASH_LEN+1], | |
| const char *timestr, const char *opaque, | |
| const digest_config_rec *conf, | |
| const char *realm) | |
| { | |
| /* The time field of a nonce is a timestamp, or with one-time nonces | |
| * an ordinal from otn_counter; hashing in which it is keeps a nonce | |
| * minted as one from verifying as the other, which a realm with both | |
| * lifetimes configured would otherwise allow: a timestamp read as an | |
| * ordinal is far beyond any the counter will reach, and once recorded | |
| * as a client's last nonce used, every genuine one-time nonce after | |
| * it is stale. */ | |
| char kind = (conf->nonce_lifetime == 0) ? 'o' : 't'; | |
| #if DIGEST_SIPHASH_NONCE | |
| unsigned char mac[APR_SIPHASH_DSIZE]; | |
| const char *msg; | |
| /* siphash takes the whole message at once, having no streaming | |
| * interface, so the fields are joined here rather than fed in one at a | |
| * time. The realm is length-prefixed, which keeps the boundaries | |
| * between the fields unambiguous whatever they contain: without it a | |
| * realm ending in what another realm's timestamp begins with would | |
| * hash the same. An absent opaque is the empty string, which is what | |
| * the challenge side passes for it too. */ | |
| msg = apr_psprintf(p, "%" APR_SIZE_T_FMT ":%s:%s:%c:%s", | |
| (apr_size_t)strlen(realm), realm, timestr, kind, | |
| opaque ? opaque : ""); | |
| apr_siphash24_auth(mac, msg, strlen(msg), secret); | |
| ap_bin2hex(mac, APR_SIPHASH_DSIZE, hash); | |
| #else | |
| unsigned char sha1[APR_SHA1_DIGESTSIZE]; | |
| apr_sha1_ctx_t ctx; | |
| apr_sha1_init(&ctx); | |
| apr_sha1_update_binary(&ctx, secret, SECRET_LEN); | |
| apr_sha1_update_binary(&ctx, (const unsigned char *) realm, strlen(realm)); | |
| apr_sha1_update_binary(&ctx, (const unsigned char *) timestr, strlen(timestr)); | |
| apr_sha1_update_binary(&ctx, (const unsigned char *) &kind, 1); | |
| if (opaque) { | |
| apr_sha1_update_binary(&ctx, (const unsigned char *) opaque, | |
| strlen(opaque)); | |
| } | |
| apr_sha1_final(sha1, &ctx); | |
| ap_bin2hex(sha1, APR_SHA1_DIGESTSIZE, hash); | |
| #endif | |
| } | |
| /* The nonce has the format b64(time)+hash . | |
| */ | |
| static const char *gen_nonce(apr_pool_t *p, apr_time_t now, const char *opaque, | |
| const digest_config_rec *conf, | |
| const char *realm) | |
| { | |
| char *nonce = apr_palloc(p, NONCE_LEN+1); | |
| time_rec t; | |
| if (conf->nonce_lifetime != 0) { | |
| t.time = now; | |
| } | |
| else { | |
| /* One-time nonces are ordered by this counter rather than by time. | |
| * It is 64-bit and issued under the lock rather than with 32-bit | |
| * atomics: a counter which wrapped would re-issue ordinals below | |
| * those already recorded, locking every established client out. A | |
| * nonce time of zero means "no nonce used yet" in a client entry, | |
| * so the first value handed out is 1. */ | |
| apr_global_mutex_lock(client_lock); | |
| t.time = ++client_list->otn_counter; | |
| apr_global_mutex_unlock(client_lock); | |
| } | |
| apr_base64_encode_binary(nonce, t.arr, sizeof(t.arr)); | |
| gen_nonce_hash(p, nonce+NONCE_TIME_LEN, nonce, opaque, conf, realm); | |
| return nonce; | |
| } | |
| /* Authorization challenge generation (for WWW-Authenticate). */ | |
| /* Format a client id as the opaque sent to the client. Never called with | |
| * zero: the callers check client_generate() for failure first. */ | |
| static const char *client_id_to_opaque(apr_pool_t *p, client_id_t num) | |
| { | |
| return apr_psprintf(p, "%" APR_UINT64_T_HEX_FMT, num); | |
| } | |
| /* Generate a challenge for the client, and return the status which the | |
| * caller should return for this request: HTTP_UNAUTHORIZED normally, or | |
| * HTTP_SERVICE_UNAVAILABLE if the per-client state which this configuration | |
| * requires could not be allocated. No challenge is sent in that case: it | |
| * could only carry an opaque which identifies nothing, so the client would | |
| * be unable to authenticate through it however often it retried. */ | |
| static int note_digest_auth_failure(request_rec *r, | |
| const digest_config_rec *conf, | |
| digest_header_rec *resp, int stale) | |
| { | |
| const char *opaque = NULL, *opaque_param = "", *domain, *nonce; | |
| client_id_t client_key; | |
| /* Setup opaque */ | |
| if (resp->opaque == NULL) { | |
| /* new client */ | |
| if (tracks_clients(conf)) { | |
| if ((client_key = client_generate(r, 0)) == 0) { | |
| return HTTP_SERVICE_UNAVAILABLE; | |
| } | |
| opaque = client_id_to_opaque(r->pool, client_key); | |
| } | |
| /* else this configuration tracks no per-client state, so no entry | |
| * is allocated and no opaque is sent */ | |
| } | |
| else if (!client_exists(resp->opaque_num, r)) { | |
| /* We have no record of this client: its entry may have been | |
| * garbage collected, the segment may have been recreated by a | |
| * restart, or the opaque may never have been issued by us. It gets | |
| * a new entry; whether the challenge is stale is the caller's | |
| * call, since credentials which failed are no less wrong for the | |
| * opaque being unknown. */ | |
| if ((client_key = client_generate(r, 1)) == 0) { | |
| return HTTP_SERVICE_UNAVAILABLE; | |
| } | |
| opaque = client_id_to_opaque(r->pool, client_key); | |
| } | |
| else { | |
| /* Note that the nonce-count tracked for this client is left alone | |
| * here: the client may not even see this challenge (it may have | |
| * been triggered by somebody else quoting its opaque), and it is | |
| * tied to the nonce it was counted for in any case. */ | |
| opaque = resp->opaque; | |
| } | |
| if (opaque) { | |
| opaque_param = apr_pstrcat(r->pool, ", opaque=\"", opaque, "\"", NULL); | |
| } | |
| /* Setup nonce */ | |
| nonce = gen_nonce(r->pool, r->request_time, opaque, conf, ap_auth_name(r)); | |
| /* Setup domain, which tells the client which URIs share this | |
| * protection space, so that it does not send the Authorization header | |
| * (usually more than 200 bytes) where it is not needed. */ | |
| if (r->proxyreq == PROXYREQ_PROXY || !conf->uri_list) { | |
| domain = NULL; | |
| } | |
| else { | |
| domain = conf->uri_list; | |
| } | |
| /* A challenge supersedes any Authentication-Info added at fixups | |
| * for this request, e.g. when a handler notes an auth failure | |
| * after the user was authenticated: the client must not be given | |
| * both a nextnonce and a new nonce. */ | |
| apr_table_unset(r->err_headers_out, | |
| (PROXYREQ_PROXY == r->proxyreq) | |
| ? "Proxy-Authentication-Info" : "Authentication-Info"); | |
| apr_table_mergen(r->err_headers_out, | |
| (PROXYREQ_PROXY == r->proxyreq) | |
| ? "Proxy-Authenticate" : "WWW-Authenticate", | |
| apr_psprintf(r->pool, "Digest realm=\"%s\", " | |
| "nonce=\"%s\", algorithm=%s%s%s%s" | |
| ", qop=\"auth\"", | |
| ap_auth_name(r), nonce, conf->algorithm, | |
| opaque_param, | |
| domain ? domain : "", | |
| stale ? ", stale=true" : "")); | |
| return HTTP_UNAUTHORIZED; | |
| } | |
| static int hook_note_digest_auth_failure(request_rec *r, const char *auth_type) | |
| { | |
| digest_header_rec *resp; | |
| digest_config_rec *conf; | |
| if (ap_cstr_casecmp(auth_type, "Digest")) | |
| return DECLINED; | |
| resp = get_digest_rec(r); | |
| resp->needed_auth = 1; | |
| conf = (digest_config_rec *) ap_get_module_config(r->per_dir_config, | |
| &auth_digest_module); | |
| note_digest_auth_failure(r, conf, resp, 0); | |
| return OK; | |
| } | |
| /* Look up the stored HA1 hash, md5(user:realm:password), for user through | |
| * the configured authn providers. Returns the provider's authn_status and, | |
| * on success, the hash in *rethash. */ | |
| static authn_status get_hash(request_rec *r, const char *user, | |
| digest_config_rec *conf, const char **rethash) | |
| { | |
| authn_status auth_result; | |
| char *password; | |
| authn_provider_list *current_provider; | |
| current_provider = conf->providers; | |
| do { | |
| const authn_provider *provider; | |
| /* For now, if a provider isn't set, we'll be nice and use the file | |
| * provider. | |
| */ | |
| if (!current_provider) { | |
| provider = ap_lookup_provider(AUTHN_PROVIDER_GROUP, | |
| AUTHN_DEFAULT_PROVIDER, | |
| AUTHN_PROVIDER_VERSION); | |
| if (!provider || !provider->get_realm_hash) { | |
| ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01770) | |
| "No Authn provider configured"); | |
| auth_result = AUTH_GENERAL_ERROR; | |
| break; | |
| } | |
| apr_table_setn(r->notes, AUTHN_PROVIDER_NAME_NOTE, AUTHN_DEFAULT_PROVIDER); | |
| } | |
| else { | |
| provider = current_provider->provider; | |
| apr_table_setn(r->notes, AUTHN_PROVIDER_NAME_NOTE, current_provider->provider_name); | |
| } | |
| /* We expect the password to be md5 hash of user:realm:password */ | |
| auth_result = provider->get_realm_hash(r, user, ap_auth_name(r), | |
| &password); | |
| apr_table_unset(r->notes, AUTHN_PROVIDER_NAME_NOTE); | |
| /* Something occurred. Stop checking. */ | |
| if (auth_result != AUTH_USER_NOT_FOUND) { | |
| break; | |
| } | |
| /* If we're not really configured for providers, stop now. */ | |
| if (!conf->providers) { | |
| break; | |
| } | |
| current_provider = current_provider->next; | |
| } while (current_provider); | |
| if (auth_result == AUTH_USER_FOUND) { | |
| *rethash = password; | |
| } | |
| return auth_result; | |
| } | |
| /* Check the nonce of a fully verified request, record its use, and | |
| * generate a new challenge if it is not acceptable. | |
| * | |
| * This runs only once the response digest has been verified, so the | |
| * stale=true it answers with never reveals whether the credentials were | |
| * right. The nonce itself is checked first - that this server issued it, | |
| * for this opaque and realm, and that it is within its lifetime - and only | |
| * then its use by the client, so that the state tracked for a client is | |
| * altered only by a request using a nonce this server issued, and a request | |
| * using an expired nonce gets a stale challenge (and hence a silent retry | |
| * with a fresh nonce-count) rather than being refused as a replay. | |
| * | |
| * The nonce-count is counted by the client per-nonce (RFC 7616 3.4.3), so | |
| * the count tracked here is tied to the nonce it was counted for: a request | |
| * using a newer nonce starts a new count. Within a single nonce the count | |
| * must strictly increase, but it need not increase by exactly one: the | |
| * client also counts the requests it sends to URIs in the protection space | |
| * which turn out not to need authentication, and this server never sees | |
| * those. | |
| */ | |
| static int check_nonce(request_rec *r, digest_header_rec *resp, | |
| const digest_config_rec *conf) | |
| { | |
| time_rec nonce_time; | |
| char tmp, hash[NONCE_HASH_LEN+1]; | |
| int len; | |
| unsigned long tracked_nc; | |
| /* The decoder stops at the first byte which is not base64, so a | |
| * nonce of the right shape can still decode to fewer bytes than the | |
| * time field holds, leaving the rest of it unset. VALID_NONCE() | |
| * bounds what it can decode to at most sizeof(nonce_time.arr). */ | |
| tmp = resp->nonce[NONCE_TIME_LEN]; | |
| resp->nonce[NONCE_TIME_LEN] = '\0'; | |
| len = apr_base64_decode_binary(nonce_time.arr, resp->nonce); | |
| gen_nonce_hash(r->pool, hash, resp->nonce, resp->opaque, conf, | |
| ap_auth_name(r)); | |
| resp->nonce[NONCE_TIME_LEN] = tmp; | |
| if (len != sizeof(nonce_time.arr)) { | |
| ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(10678) | |
| "invalid nonce %s received - time is not base64", | |
| resp->nonce); | |
| return note_digest_auth_failure(r, conf, resp, 1); | |
| } | |
| resp->nonce_time = nonce_time.time; | |
| if (!ap_memeq_timingsafe(hash, resp->nonce+NONCE_TIME_LEN, NONCE_HASH_LEN)) { | |
| ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01776) | |
| "invalid nonce %s received - hash is not %s", | |
| resp->nonce, hash); | |
| return note_digest_auth_failure(r, conf, resp, 1); | |
| } | |
| if (conf->nonce_lifetime > 0) { | |
| apr_time_t dt = r->request_time - nonce_time.time; | |
| if (dt < 0) { | |
| ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01777) | |
| "invalid nonce %s received - user attempted " | |
| "time travel", resp->nonce); | |
| return note_digest_auth_failure(r, conf, resp, 1); | |
| } | |
| if (dt > conf->nonce_lifetime) { | |
| ap_log_rerror(APLOG_MARK, APLOG_INFO, 0,r, APLOGNO(01778) | |
| "user %s: nonce expired (%.2f seconds old " | |
| "- max lifetime %.2f) - sending new nonce", | |
| r->user, (double)apr_time_sec(dt), | |
| (double)apr_time_sec(conf->nonce_lifetime)); | |
| return note_digest_auth_failure(r, conf, resp, 1); | |
| } | |
| } | |
| if (!tracks_clients(conf)) { | |
| return OK; /* a timed nonce, not counted: that's all */ | |
| } | |
| /* A one-time nonce is retired by its use, and with AuthDigestNcCheck | |
| * the count must rise: both are checked against, and recorded in, the | |
| * state tracked for the client. */ | |
| switch (client_update_nonce(r, resp->opaque_num, conf, nonce_time.time, | |
| resp->nc, &tracked_nc)) { | |
| case NONCE_ACCEPTED: | |
| return OK; | |
| case NONCE_UNKNOWN: | |
| ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r, APLOGNO(10618) | |
| "client %" APR_UINT64_T_FMT " is no longer known - " | |
| "sending new nonce", resp->opaque_num); | |
| break; | |
| case NONCE_STALE: | |
| ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r, APLOGNO(01779) | |
| "user %s: one-time-nonce %s already used - sending " | |
| "new nonce", r->user, resp->nonce); | |
| break; | |
| case NONCE_BAD_COUNT: | |
| ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r, APLOGNO(01774) | |
| "user %s: nonce-count %lu is not above %lu for nonce " | |
| "%s - sending new nonce", r->user, resp->nc, tracked_nc, | |
| resp->nonce); | |
| break; | |
| case NONCE_NOT_OWNER: | |
| ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r, APLOGNO(10679) | |
| "user %s: client %" APR_UINT64_T_FMT " belongs to " | |
| "another user - sending new nonce and opaque", | |
| r->user, resp->opaque_num); | |
| /* dropping the opaque has the challenge issue this user an entry of | |
| * its own rather than the one it quoted. Both forms go, since the | |
| * record is shared with any later request in this tree. */ | |
| resp->opaque = NULL; | |
| resp->opaque_num = 0; | |
| break; | |
| } | |
| /* the credentials were good, so the client can silently retry with the | |
| * nonce from this challenge. A nonce-count which has not increased is | |
| * what concurrent requests from one client look like when they arrive | |
| * out of order, and a replay gains nothing from the stale flag, having | |
| * no way to answer the new nonce. */ | |
| return note_digest_auth_failure(r, conf, resp, 1); | |
| } | |
| /* Compute the response digest expected for request r (RFC 2617). */ | |
| static const char *new_digest(const request_rec *r, | |
| digest_header_rec *resp) | |
| { | |
| const char *ha1, *ha2, *a2; | |
| ha1 = resp->ha1; | |
| a2 = apr_pstrcat(r->pool, resp->method, ":", resp->uri, NULL); | |
| ha2 = ap_md5(r->pool, (const unsigned char *)a2); | |
| return ap_md5(r->pool, | |
| (unsigned char *)apr_pstrcat(r->pool, ha1, ":", resp->nonce, | |
| ":", resp->nonce_count, ":", | |
| resp->cnonce, ":", | |
| resp->message_qop, ":", ha2, | |
| NULL)); | |
| } | |
| static int copy_uri_components(apr_uri_t *dst, | |
| apr_uri_t *src, request_rec *r) { | |
| int rv; | |
| if (src->scheme && src->scheme[0] != '\0') { | |
| dst->scheme = src->scheme; | |
| } | |
| else { | |
| dst->scheme = (char *) "http"; | |
| } | |
| if (src->hostname && src->hostname[0] != '\0') { | |
| dst->hostname = apr_pstrdup(r->pool, src->hostname); | |
| if ((rv = ap_unescape_url(dst->hostname)) != OK) { | |
| return rv; | |
| } | |
| } | |
| else { | |
| dst->hostname = (char *) ap_get_server_name(r); | |
| } | |
| if (src->port_str && src->port_str[0] != '\0') { | |
| dst->port = src->port; | |
| } | |
| else { | |
| dst->port = ap_get_server_port(r); | |
| } | |
| if (src->path && src->path[0] != '\0') { | |
| dst->path = apr_pstrdup(r->pool, src->path); | |
| if ((rv = ap_unescape_url(dst->path)) != OK) { | |
| return rv; | |
| } | |
| } | |
| else { | |
| dst->path = src->path; | |
| } | |
| if (src->query && src->query[0] != '\0') { | |
| dst->query = apr_pstrdup(r->pool, src->query); | |
| if ((rv = ap_unescape_url(dst->query)) != OK) { | |
| return rv; | |
| } | |
| } | |
| else { | |
| dst->query = src->query; | |
| } | |
| dst->hostinfo = src->hostinfo; | |
| return OK; | |
| } | |
| /* These functions return 0 if client is OK, and proper error status | |
| * if not... either HTTP_UNAUTHORIZED, if we made a check, and it failed, or | |
| * HTTP_INTERNAL_SERVER_ERROR, if things are so totally confused that we | |
| * couldn't figure out how to tell if the client is authorized or not. | |
| * | |
| * If they return DECLINED, and all other modules also decline, that's | |
| * treated by the server core as a configuration error, logged and | |
| * reported as such. | |
| */ | |
| /* Determine user ID, and check if the attributes are correct, if it | |
| * really is that user, if the nonce is correct, etc. | |
| */ | |
| static int authenticate_digest_user(request_rec *r) | |
| { | |
| digest_config_rec *conf; | |
| digest_header_rec *resp; | |
| const char *t; | |
| authn_status return_code; | |
| const char *realm; | |
| /* do we require Digest auth for this URI? */ | |
| if (!(t = ap_auth_type(r)) || ap_cstr_casecmp(t, "Digest")) { | |
| return DECLINED; | |
| } | |
| if (!ap_auth_name(r)) { | |
| ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01780) | |
| "need AuthName: %s", r->uri); | |
| return HTTP_INTERNAL_SERVER_ERROR; | |
| } | |
| resp = get_digest_rec(r); | |
| resp->needed_auth = 1; | |
| realm = ap_auth_name(r); | |
| conf = (digest_config_rec *) ap_get_module_config(r->per_dir_config, | |
| &auth_digest_module); | |
| /* Check for existence and syntax of the Auth header. */ | |
| if (resp->auth_hdr_sts != VALID) { | |
| if (resp->auth_hdr_sts == NOT_DIGEST) { | |
| ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01781) | |
| "client used wrong authentication scheme `%s': %s", | |
| resp->scheme, r->uri); | |
| } | |
| else if (resp->auth_hdr_sts == INVALID) { | |
| ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01782) | |
| "missing user, realm, nonce, uri, digest, " | |
| "cnonce, or nonce_count, or invalid opaque, in " | |
| "authorization header: %s", r->uri); | |
| } | |
| /* else (resp->auth_hdr_sts == NO_HEADER) */ | |
| return note_digest_auth_failure(r, conf, resp, 0); | |
| } | |
| r->user = (char *) resp->username; | |
| r->ap_auth_type = (char *) "Digest"; | |
| /* check the auth attributes */ | |
| if (strcmp(resp->uri, resp->raw_request_uri)) { | |
| /* Hmm, the simple match didn't work (probably a proxy modified the | |
| * request-uri), so lets do a more sophisticated match | |
| */ | |
| apr_uri_t r_uri, d_uri; | |
| if (copy_uri_components(&r_uri, resp->psd_request_uri, r) != OK) { | |
| ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(10635) | |
| "invalid request-uri <%s>", r->unparsed_uri); | |
| return HTTP_BAD_REQUEST; | |
| } | |
| if (apr_uri_parse(r->pool, resp->uri, &d_uri) != APR_SUCCESS | |
| || (d_uri.hostname && ap_unescape_url(d_uri.hostname) != OK) | |
| || (d_uri.path && ap_unescape_url(d_uri.path) != OK) | |
| || (d_uri.query && ap_unescape_url(d_uri.query) != OK)) { | |
| ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01783) | |
| "invalid uri <%s> in Authorization header", | |
| resp->uri); | |
| return HTTP_BAD_REQUEST; | |
| } | |
| if (r->method_number == M_CONNECT) { | |
| if (!r_uri.hostinfo || strcmp(resp->uri, r_uri.hostinfo)) { | |
| ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01785) | |
| "uri mismatch - <%s> does not match " | |
| "request-uri <%s>", resp->uri, r_uri.hostinfo); | |
| return HTTP_BAD_REQUEST; | |
| } | |
| } | |
| else if ( | |
| /* check hostname matches, if present */ | |
| (d_uri.hostname && d_uri.hostname[0] != '\0' | |
| && ap_cstr_casecmp(d_uri.hostname, r_uri.hostname)) | |
| /* check port matches, if present */ | |
| || (d_uri.port_str && d_uri.port != r_uri.port) | |
| /* check that server-port is default port if no port present */ | |
| || (d_uri.hostname && d_uri.hostname[0] != '\0' | |
| && !d_uri.port_str && r_uri.port != ap_default_port(r)) | |
| /* check that path matches */ | |
| || (d_uri.path != r_uri.path | |
| /* either exact match */ | |
| && (!d_uri.path || !r_uri.path | |
| || strcmp(d_uri.path, r_uri.path)) | |
| /* or '*' matches empty path in scheme://host */ | |
| && !(d_uri.path && !r_uri.path && resp->psd_request_uri->hostname | |
| && d_uri.path[0] == '*' && d_uri.path[1] == '\0')) | |
| /* check that query matches */ | |
| || (d_uri.query != r_uri.query | |
| && (!d_uri.query || !r_uri.query | |
| || strcmp(d_uri.query, r_uri.query))) | |
| ) { | |
| ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01786) | |
| "uri mismatch - <%s> does not match " | |
| "request-uri <%s>", resp->uri, resp->raw_request_uri); | |
| return HTTP_BAD_REQUEST; | |
| } | |
| } | |
| if (strcmp(resp->realm, realm)) { | |
| ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01788) | |
| "realm mismatch - got `%s' but expected `%s'", | |
| resp->realm, realm); | |
| return note_digest_auth_failure(r, conf, resp, 0); | |
| } | |
| if (resp->algorithm != NULL | |
| && ap_cstr_casecmp(resp->algorithm, "MD5")) { | |
| ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01789) | |
| "unknown algorithm `%s' received: %s", | |
| resp->algorithm, r->uri); | |
| return note_digest_auth_failure(r, conf, resp, 0); | |
| } | |
| return_code = get_hash(r, r->user, conf, &resp->ha1); | |
| if (return_code == AUTH_USER_NOT_FOUND) { | |
| ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01790) | |
| "user `%s' in realm `%s' not found: %s", | |
| r->user, realm, r->uri); | |
| return note_digest_auth_failure(r, conf, resp, 0); | |
| } | |
| else if (return_code == AUTH_USER_FOUND) { | |
| /* we have a password, so continue */ | |
| } | |
| else if (return_code == AUTH_DENIED) { | |
| /* authentication denied in the provider before attempting a match */ | |
| ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01791) | |
| "user `%s' in realm `%s' denied by provider: %s", | |
| r->user, realm, r->uri); | |
| return note_digest_auth_failure(r, conf, resp, 0); | |
| } | |
| else if (return_code == AUTH_HANDLED) { | |
| return r->status; | |
| } | |
| else { | |
| /* AUTH_GENERAL_ERROR (or worse) | |
| * We'll assume that the module has already said what its error | |
| * was in the logs. | |
| */ | |
| return HTTP_INTERNAL_SERVER_ERROR; | |
| } | |
| if (resp->message_qop == NULL | |
| || ap_cstr_casecmp(resp->message_qop, "auth")) { | |
| /* RFC 2069-style Digest is no longer supported. */ | |
| ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(10560) | |
| "invalid or missing qop value '%s', RFC 2069 is " | |
| "no longer supported: %s", resp->message_qop, r->uri); | |
| return note_digest_auth_failure(r, conf, resp, 0); | |
| } | |
| else { | |
| /* RFC 2617 (or 7616)-style Digest hash calculation. */ | |
| const char *exp_digest = new_digest(r, resp); | |
| if (!ap_memeq_timingsafe(exp_digest, resp->digest, MD5_DIGEST_LEN)) { | |
| ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01794) | |
| "user %s: password mismatch: %s", r->user, | |
| r->uri); | |
| return note_digest_auth_failure(r, conf, resp, 0); | |
| } | |
| } | |
| /* Only now that the digest is verified is the nonce checked; see | |
| * check_nonce() for why. */ | |
| return check_nonce(r, resp, conf); | |
| } | |
| /* Authentication-Info header code. */ | |
| static int add_auth_info(request_rec *r) | |
| { | |
| const digest_config_rec *conf = | |
| (digest_config_rec *) ap_get_module_config(r->per_dir_config, | |
| &auth_digest_module); | |
| digest_header_rec *resp = | |
| (digest_header_rec *) ap_get_module_config(r->request_config, | |
| &auth_digest_module); | |
| const char *ai = NULL, *nextnonce = ""; | |
| if (resp == NULL || !resp->needed_auth || conf == NULL) { | |
| return OK; | |
| } | |
| /* Don't add Authentication-Info for 401/407 responses. */ | |
| if (apr_table_get(r->err_headers_out, | |
| (r->proxyreq == PROXYREQ_PROXY) | |
| ? "Proxy-Authenticate" : "WWW-Authenticate")) { | |
| return OK; | |
| } | |
| /* Set up nextnonce for one-time-nonces and expiring-nonce cases. */ | |
| if (conf->nonce_lifetime > 0) { | |
| /* send nextnonce if current nonce will expire in less than 30 secs */ | |
| if ((r->request_time - resp->nonce_time) > (conf->nonce_lifetime-NEXTNONCE_DELTA)) { | |
| nextnonce = apr_pstrcat(r->pool, ", nextnonce=\"", | |
| gen_nonce(r->pool, r->request_time, | |
| resp->opaque, conf, ap_auth_name(r)), | |
| "\"", NULL); | |
| } | |
| } | |
| else if (conf->nonce_lifetime == 0 && resp->opaque_num) { | |
| const char *nonce = gen_nonce(r->pool, 0, resp->opaque, conf, | |
| ap_auth_name(r)); | |
| nextnonce = apr_pstrcat(r->pool, ", nextnonce=\"", nonce, "\"", NULL); | |
| } | |
| /* else nonce never expires, hence no nextnonce */ | |
| { | |
| const char *resp_dig, *ha1, *a2, *ha2; | |
| /* Calculate the rspauth attribute. */ | |
| ha1 = resp->ha1; | |
| a2 = apr_pstrcat(r->pool, ":", resp->uri, NULL); | |
| ha2 = ap_md5(r->pool, (const unsigned char *)a2); | |
| resp_dig = ap_md5(r->pool, | |
| (unsigned char *)apr_pstrcat(r->pool, ha1, ":", | |
| resp->nonce, ":", | |
| resp->nonce_count, ":", | |
| resp->cnonce, ":", | |
| resp->message_qop ? | |
| resp->message_qop : "", | |
| ":", ha2, NULL)); | |
| /* Assemble the Authentication-Info header. */ | |
| ai = apr_pstrcat(r->pool, | |
| "rspauth=\"", resp_dig, "\"", | |
| nextnonce, | |
| resp->cnonce ? ", cnonce=\"" : "", | |
| resp->cnonce | |
| ? ap_escape_quotes(r->pool, resp->cnonce) | |
| : "", | |
| resp->cnonce ? "\"" : "", | |
| resp->nonce_count ? ", nc=" : "", | |
| resp->nonce_count ? resp->nonce_count : "", | |
| resp->message_qop ? ", qop=" : "", | |
| resp->message_qop ? resp->message_qop : "", | |
| NULL); | |
| } | |
| if (ai && ai[0]) { | |
| /* This must use ->err_headers_out so it survives an error | |
| * response, else a one-time-nonce client loses the nextnonce | |
| * and is stale-challenged on its next request. */ | |
| apr_table_mergen(r->err_headers_out, | |
| (PROXYREQ_PROXY == r->proxyreq) | |
| ? "Proxy-Authentication-Info" | |
| : "Authentication-Info", | |
| ai); | |
| } | |
| return OK; | |
| } | |
| static void register_hooks(apr_pool_t *p) | |
| { | |
| static const char * const cfgPost[]={ "http_core.c", NULL }; | |
| static const char * const parsePre[]={ "mod_proxy.c", NULL }; | |
| ap_hook_pre_config(pre_init, NULL, NULL, APR_HOOK_MIDDLE); | |
| ap_hook_post_config(initialize_module, NULL, cfgPost, APR_HOOK_MIDDLE); | |
| ap_hook_child_init(initialize_child, NULL, NULL, APR_HOOK_MIDDLE); | |
| ap_hook_post_read_request(init_digest_request, parsePre, NULL, APR_HOOK_MIDDLE); | |
| ap_hook_check_authn(authenticate_digest_user, NULL, NULL, APR_HOOK_MIDDLE, | |
| AP_AUTH_INTERNAL_PER_CONF); | |
| ap_hook_fixups(add_auth_info, NULL, NULL, APR_HOOK_MIDDLE); | |
| ap_hook_note_auth_failure(hook_note_digest_auth_failure, NULL, NULL, | |
| APR_HOOK_MIDDLE); | |
| } | |
| AP_DECLARE_MODULE(auth_digest) = | |
| { | |
| STANDARD20_MODULE_STUFF, | |
| create_digest_dir_config, /* dir config creater */ | |
| NULL, /* dir merger --- default is to override */ | |
| NULL, /* server config */ | |
| NULL, /* merge server config */ | |
| digest_cmds, /* command table */ | |
| register_hooks /* register hooks */ | |
| }; | |