/* * HttpAuth * * Copyright (C) 2004 Stefan Walter * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. * You should have received a copy of the GNU General Public License * along with this program; if not, write to the * Free Software Foundation, Inc., * 59 Temple Place, Suite 330, * Boston, MA 02111-1307, USA. */ /* Ideas and Guidance from Apache 2.0's mod_auth_digest */ #include "usuals.h" #include "md5.h" #include "httpauthd.h" #include "digest.h" #include "stringx.h" #include #include #include /* A globally unique counter used to guarantee uniqueness of nonces */ static unsigned int g_digest_unique = 0; /* All the various HTTP methods to try when ignoring the method */ static const char* g_http_methods[] = { "GET", "HEAD", "POST", "PUT", "DELETE", "TRACE", "CONNECT", NULL }; typedef struct internal_nonce { unsigned char hash[MD5_LEN]; time_t tm; unsigned int unique; } internal_nonce_t __attribute__((packed)); void digest_makenonce(unsigned char* nonce, unsigned char* secret, unsigned char* old) { internal_nonce_t in; md5_ctx_t md5; ASSERT(nonce && secret); ASSERT(sizeof(internal_nonce_t) == DIGEST_NONCE_LEN); if(old) { internal_nonce_t* nold = (internal_nonce_t*)old; in.tm = nold->tm; in.unique = nold->unique; } else { in.tm = time(NULL); ha_lock(NULL); in.unique = g_digest_unique++; ha_unlock(NULL); } md5_init(&md5); md5_update(&md5, secret, DIGEST_SECRET_LEN); md5_update(&md5, &(in.tm), sizeof(in.tm)); md5_update(&md5, &(in.unique), sizeof(in.unique)); md5_final(in.hash, &md5); memcpy(nonce, &in, DIGEST_NONCE_LEN); } int digest_checknonce(unsigned char* nonce, unsigned char* secret, time_t* tm) { internal_nonce_t in; ASSERT(nonce && secret); ASSERT(sizeof(internal_nonce_t) == DIGEST_NONCE_LEN); digest_makenonce((unsigned char*)&in, secret, nonce); if(memcmp((unsigned char*)&in, nonce, DIGEST_SECRET_LEN) == 0) { if(tm) *tm = in.tm; return HA_OK; } return HA_FALSE; } const char* digest_challenge(ha_buffer_t* buf, const char* nonce_str, const char* realm, const char* domains, int stale) { ASSERT(buf && realm && nonce_str); ha_bufmcat(buf, HA_PREFIX_DIGEST, " realm=\"", realm, "\", nonce=\"", nonce_str, "\", qop=\"auth\", algorithm=\"MD5\"", NULL); if(domains) { ha_bufjoin(buf); ha_bufmcat(buf, ", domain=\"", domains, "\"", NULL); } if(stale) { ha_bufjoin(buf); ha_bufcat(buf, ", stale=true"); } return ha_bufdata(buf); } /* * Copyright 1999-2004 The Apache Software Foundation * * Licensed 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. */ int digest_parse(const char* head, ha_buffer_t* buf, digest_header_t* rec) { char next; char* key; char* value; char* header; ASSERT(head && buf && rec); header = ha_bufcpy(buf, head); if(!header) return HA_CRITERROR; memset(rec, 0, sizeof(*rec)); while(header[0]) { /* find key */ header = (char*)trim_start(header); key = header; while(header[0] && header[0] != '=' && header[0] != ',' && !isspace(header[0])) header++; /* null terminate and move on */ next = header[0]; header[0] = '\0'; header++; if(!next) break; if(isspace(header[0])) { header = (char*)trim_start(header); next = header[0]; } /* find value */ if(next == '=') { header = (char*)trim_start(header); if(header[0] == '\"') /* quoted string */ { header++; value = header; while(header[0] && header[0] != '\"') header++; next = header[0]; header[0] = 0; header++; } else /* token */ { value = header; while(header[0] && header[0] != ',' && !isspace(header[0])) header++; next = header[0]; header[0] = 0; header++; } if(next != ',') { while(header[0] && header[0] != ',') header++; if(header[0]) header++; } if(!strcasecmp(key, "username")) rec->username = value; else if(!strcasecmp(key, "realm")) rec->realm = value; else if (!strcasecmp(key, "nonce")) rec->nonce = value; else if (!strcasecmp(key, "uri")) rec->uri = value; else if (!strcasecmp(key, "response")) rec->digest = value; else if (!strcasecmp(key, "algorithm")) rec->algorithm = value; else if (!strcasecmp(key, "cnonce")) rec->cnonce = value; else if (!strcasecmp(key, "opaque")) rec->opaque = value; else if (!strcasecmp(key, "qop")) rec->qop = value; else if (!strcasecmp(key, "nc")) rec->nc = value; } } return HA_OK; } int digest_check(digest_context_t* dg, const ha_context_t* opts, ha_buffer_t* buf, int *stale) { int r; r = digest_pre_check(dg, opts, buf, stale); if(r == HA_OK) r = digest_complete_check(dg, opts, buf); return r; } int digest_pre_check(digest_context_t* dg, const ha_context_t* opts, ha_buffer_t* buf, int *stale) { ASSERT(buf && buf && dg); ASSERT(stale); /* Check for digest */ if(!dg->client.digest || !dg->client.digest[0]) { ha_messagex(NULL, LOG_WARNING, "digest response missing digest"); return HA_BADREQ; } /* Username */ if(!dg->client.username || !dg->client.username[0]) { ha_messagex(NULL, LOG_WARNING, "digest response missing username"); return HA_BADREQ; } /* The realm */ if(!dg->client.realm) { ha_messagex(NULL, LOG_WARNING, "digest response contains missing realm"); return HA_BADREQ; } if(strcmp(dg->client.realm, opts->realm) != 0) { ha_messagex(NULL, LOG_ERR, "digest response contains invalid realm: %s", dg->client.realm); return HA_FALSE; } /* Components in the new RFC */ if(dg->client.qop) { /* * We only support 'auth' qop. We don't have access to the data * and wouldn't be able to support anything else. */ if(strcmp(dg->client.qop, "auth") != 0) { ha_messagex(NULL, LOG_WARNING, "digest response contains unknown or unsupported qop: '%s'", dg->client.qop ? dg->client.qop : ""); return HA_BADREQ; } /* The cnonce */ if(!dg->client.cnonce || !dg->client.cnonce[0]) { ha_messagex(NULL, LOG_WARNING, "digest response is missing cnonce value"); return HA_BADREQ; } if(!opts->digest_ignorenc) { /* The nonce count */ if(!dg->client.nc || !dg->client.nc[0]) { ha_messagex(NULL, LOG_WARNING, "digest response is missing nc value"); return HA_BADREQ; } /* Validate the nc */ else { /* TODO: We need to validate the nc properly. Somehow that's not * happening here. */ char* e; long nc = strtol(dg->client.nc, &e, 16); if(*e) { ha_messagex(NULL, LOG_ERR, "digest response has invalid nc value: %s", dg->client.nc); return HA_BADREQ; } /* If we didn't a nc then save it away */ if(!*e && dg->server_nc == 0) dg->server_nc = nc; if(*e || nc != dg->server_nc) { ha_messagex(NULL, LOG_WARNING, "digest response has wrong nc value: %s " "possible replay attack, should be: %d", dg->client.nc, dg->server_nc); *stale = 1; return HA_FALSE; } } } } /* The algorithm */ if(dg->client.algorithm && strcasecmp(dg->client.algorithm, "MD5") != 0) { ha_messagex(NULL, LOG_WARNING, "digest response contains unknown or unsupported algorithm: '%s'", dg->client.algorithm ? dg->client.algorithm : ""); return HA_BADREQ; } /* Request URI */ if(!dg->client.uri) { ha_messagex(NULL, LOG_WARNING, "digest response is missing uri"); return HA_BADREQ; } if(!opts->digest_allowany && strcmp(dg->client.uri, dg->server_uri) != 0) { ha_uri_t d_uri; ha_uri_t s_uri; if(ha_uriparse(buf, dg->client.uri, &d_uri) < 0) { if(CHECK_BUF(buf)) return HA_CRITERROR; ha_messagex(NULL, LOG_WARNING, "digest response constains invalid uri: %s", dg->client.uri); return HA_BADREQ; } ASSERT(dg->server_uri); if(ha_uriparse(buf, dg->server_uri, &s_uri) < 0) { if(CHECK_BUF(buf)) return HA_CRITERROR; ha_messagex(NULL, LOG_ERR, "server sent us an invalid uri: %s", dg->server_uri); return HA_BADREQ; } if(ha_uricmp(&d_uri, &s_uri) != 0) { ha_messagex(NULL, LOG_ERR, "digest response contains wrong uri: %s " "(should be %s)", dg->client.uri, dg->server_uri); return HA_FALSE; } } /* * nonce: should have already been validated by the caller who * found this nice rec structure to pass us. * * opaque: We also don't use opaque. The caller should have validated it * if it's used there. */ return HA_OK; } static int internal_check(digest_context_t* dg, const char* http_method, ha_buffer_t* buf) { unsigned char hash[MD5_LEN]; md5_ctx_t md5; const char* t; const char* digest; ASSERT(dg && buf); /* * Now we validate the digest response */ /* Encode ha1 */ t = ha_bufenchex(buf, dg->server_ha1, MD5_LEN); if(t == NULL) return HA_CRITERROR; ASSERT(dg->server_method); /* Encode ha2 */ md5_init(&md5); md5_update(&md5, http_method, strlen(http_method)); md5_update(&md5, ":", 1); md5_update(&md5, dg->client.uri, strlen(dg->client.uri)); md5_final(hash, &md5); ha_bufenchex(buf, hash, MD5_LEN); if(!ha_bufdata(buf)) return HA_CRITERROR; ASSERT(dg->client.nonce); /* Old style digest (RFC 2069) */ if(!dg->client.qop) { md5_init(&md5); md5_update(&md5, t, MD5_LEN * 2); /* ha1 */ md5_update(&md5, ":", 1); md5_update(&md5, dg->client.nonce, strlen(dg->client.nonce)); /* nonce */ md5_update(&md5, ":", 1); md5_update(&md5, ha_bufdata(buf), MD5_LEN * 2); /* ha1 */ md5_final(hash, &md5); } /* New style 'auth' digest (RFC 2617) */ else { ASSERT(dg->client.nc); ASSERT(dg->client.cnonce); ASSERT(dg->client.qop); md5_init(&md5); md5_update(&md5, t, MD5_LEN * 2); /* ha1 */ md5_update(&md5, ":", 1); md5_update(&md5, dg->client.nonce, strlen(dg->client.nonce)); /* nonce */ md5_update(&md5, ":", 1); md5_update(&md5, dg->client.nc, strlen(dg->client.nc)); /* nc */ md5_update(&md5, ":", 1); md5_update(&md5, dg->client.cnonce, strlen(dg->client.cnonce)); /* cnonce */ md5_update(&md5, ":", 1); md5_update(&md5, dg->client.qop, strlen(dg->client.qop)); /* qop */ md5_update(&md5, ":", 1); md5_update(&md5, ha_bufdata(buf), MD5_LEN * 2); /* ha2 */ md5_final(hash, &md5); } /* Encode the digest */ digest = ha_bufenchex(buf, hash, MD5_LEN); if(digest == NULL) return HA_CRITERROR; ASSERT(dg->client.digest); if(strcasecmp(dg->client.digest, digest) != 0) return HA_FALSE; return HA_OK; } int digest_complete_check(digest_context_t* dg, const ha_context_t* opts, ha_buffer_t* buf) { const char** m; int ret; /* Use the method sent to us */ ret = internal_check (dg, dg->server_method, buf); if(ret != HA_FALSE) return ret; if(opts->digest_allowany) { /* Try out each and every method in HTTP */ for(m = g_http_methods; *m; ++m) { ret = internal_check (dg, *m, buf); if(ret != HA_FALSE) break; } } return ret; } const char* digest_respond(digest_context_t* dg, ha_buffer_t* buf, unsigned char* next) { unsigned char hash[MD5_LEN]; md5_ctx_t md5; const char* nextnonce = NULL; const char* t; ASSERT(buf && dg); /* This makes a new buffer */ ha_bufcpy(buf, ""); if(next) { nextnonce = ha_bufenc64(buf, next, DIGEST_NONCE_LEN); if(nextnonce == NULL) return NULL; } /* For older clients RFC 2069 */ if(!dg->client.qop) { if(nextnonce) ha_bufmcat(buf, "nextnonce=\"", nextnonce, "\"", NULL); return ha_bufdata(buf); } /* Otherwise we do the whole song and dance */ /* Encode ha1 */ t = ha_bufenchex(buf, dg->server_ha1, MD5_LEN); if(t == NULL) return NULL; ASSERT(dg->client.uri); /* Encode ha2 */ md5_init(&md5); md5_update(&md5, ":", 1); md5_update(&md5, dg->client.uri, strlen(dg->client.uri)); md5_final(hash, &md5); ha_bufenchex(buf, hash, MD5_LEN); if(!ha_bufdata(buf)) return NULL; ASSERT(dg->client.nonce); ASSERT(dg->client.nc); ASSERT(dg->client.cnonce); ASSERT(dg->client.qop); /* New style 'auth' digest (RFC 2617) */ md5_init(&md5); md5_update(&md5, t, MD5_LEN * 2); /* ha1 */ md5_update(&md5, ":", 1); md5_update(&md5, dg->client.nonce, strlen(dg->client.nonce)); /* nonce */ md5_update(&md5, ":", 1); md5_update(&md5, dg->client.nc, strlen(dg->client.nc)); /* nc */ md5_update(&md5, ":", 1); md5_update(&md5, dg->client.cnonce, strlen(dg->client.cnonce)); /* cnonce */ md5_update(&md5, ":", 1); md5_update(&md5, dg->client.qop, strlen(dg->client.qop)); /* qop */ md5_update(&md5, ":", 1); md5_update(&md5, ha_bufdata(buf), MD5_LEN * 2); /* ha2 */ md5_final(hash, &md5); /* Encode the digest */ t = ha_bufenchex(buf, hash, MD5_LEN); if(t == NULL) return NULL; ha_bufmcat(buf, "rspauth=\"", t, "\"", ", qop=", dg->client.qop, ", nc=", dg->client.nc, ", cnonce=\"", dg->client.cnonce, "\"", NULL); if(nextnonce) { ha_bufjoin(buf); ha_bufmcat(buf, ", nextnonce=\"", nextnonce, "\"", NULL); } return ha_bufdata(buf); } void digest_makeha1(unsigned char* digest, const char* user, const char* realm, const char* password) { md5_ctx_t md5; ASSERT(digest && user && realm && password); md5_init(&md5); md5_update(&md5, user, strlen(user)); md5_update(&md5, ":", 1); md5_update(&md5, realm, strlen(realm)); md5_update(&md5, ":", 1); md5_update(&md5, password, strlen(password)); md5_final(digest, &md5); } #define MUST_ESCAPE "\"\' \t\n\r\v\\" void digest_escape (ha_buffer_t *buf, const char *orig) { const char* t; size_t pos; assert (orig); assert (buf); ha_bufcpy(buf, ""); t = orig; while (*t) { pos = strcspn (t, MUST_ESCAPE); if(pos > 0) { ha_bufjoin (buf); ha_bufncpy (buf, t, pos); t += pos; } while (*t && !strchr (MUST_ESCAPE, *t)) { char esc[3]; esc[0] = '\\'; esc[1] = *t; esc[2] = '\0'; ha_bufjoin (buf); ha_bufcpy (buf, esc); t++; } } }