auth_digest.c revision 362181
1/* ==================================================================== 2 * Licensed to the Apache Software Foundation (ASF) under one 3 * or more contributor license agreements. See the NOTICE file 4 * distributed with this work for additional information 5 * regarding copyright ownership. The ASF licenses this file 6 * to you under the Apache License, Version 2.0 (the 7 * "License"); you may not use this file except in compliance 8 * with the License. You may obtain a copy of the License at 9 * 10 * http://www.apache.org/licenses/LICENSE-2.0 11 * 12 * Unless required by applicable law or agreed to in writing, 13 * software distributed under the License is distributed on an 14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 * KIND, either express or implied. See the License for the 16 * specific language governing permissions and limitations 17 * under the License. 18 * ==================================================================== 19 */ 20 21/*** Digest authentication ***/ 22 23#include <serf.h> 24#include <serf_private.h> 25#include <auth/auth.h> 26 27#include <apr.h> 28#include <apr_base64.h> 29#include <apr_strings.h> 30#include <apr_uuid.h> 31#include <apr_md5.h> 32 33/** Digest authentication, implements RFC 2617. **/ 34 35/* TODO: add support for the domain attribute. This defines the protection 36 space, so that serf can decide per URI if it should reuse the cached 37 credentials for the server, or not. */ 38 39/* Stores the context information related to Digest authentication. 40 This information is stored in the per server cache in the serf context. */ 41typedef struct digest_authn_info_t { 42 /* nonce-count for digest authentication */ 43 unsigned int digest_nc; 44 45 const char *header; 46 47 const char *ha1; 48 49 const char *realm; 50 const char *cnonce; 51 const char *nonce; 52 const char *opaque; 53 const char *algorithm; 54 const char *qop; 55 const char *username; 56 57 apr_pool_t *pool; 58} digest_authn_info_t; 59 60static char 61int_to_hex(int v) 62{ 63 return (v < 10) ? '0' + v : 'a' + (v - 10); 64} 65 66/** 67 * Convert a string if ASCII characters HASHVAL to its hexadecimal 68 * representation. 69 * 70 * The returned string will be allocated in the POOL and be null-terminated. 71 */ 72static const char * 73hex_encode(const unsigned char *hashval, 74 apr_pool_t *pool) 75{ 76 int i; 77 char *hexval = apr_palloc(pool, (APR_MD5_DIGESTSIZE * 2) + 1); 78 for (i = 0; i < APR_MD5_DIGESTSIZE; i++) { 79 hexval[2 * i] = int_to_hex((hashval[i] >> 4) & 0xf); 80 hexval[2 * i + 1] = int_to_hex(hashval[i] & 0xf); 81 } 82 hexval[APR_MD5_DIGESTSIZE * 2] = '\0'; 83 return hexval; 84} 85 86/** 87 * Returns a 36-byte long string of random characters. 88 * UUIDs are formatted as: 00112233-4455-6677-8899-AABBCCDDEEFF. 89 * 90 * The returned string will be allocated in the POOL and be null-terminated. 91 */ 92static const char * 93random_cnonce(apr_pool_t *pool) 94{ 95 apr_uuid_t uuid; 96 char *buf = apr_palloc(pool, APR_UUID_FORMATTED_LENGTH + 1); 97 98 apr_uuid_get(&uuid); 99 apr_uuid_format(buf, &uuid); 100 101 return hex_encode((unsigned char*)buf, pool); 102} 103 104static apr_status_t 105build_digest_ha1(const char **out_ha1, 106 const char *username, 107 const char *password, 108 const char *realm_name, 109 apr_pool_t *pool) 110{ 111 const char *tmp; 112 unsigned char ha1[APR_MD5_DIGESTSIZE]; 113 apr_status_t status; 114 115 /* calculate ha1: 116 MD5 hash of the combined user name, authentication realm and password */ 117 tmp = apr_psprintf(pool, "%s:%s:%s", 118 username, 119 realm_name, 120 password); 121 status = apr_md5(ha1, tmp, strlen(tmp)); 122 if (status) 123 return status; 124 125 *out_ha1 = hex_encode(ha1, pool); 126 127 return APR_SUCCESS; 128} 129 130static apr_status_t 131build_digest_ha2(const char **out_ha2, 132 const char *uri, 133 const char *method, 134 const char *qop, 135 apr_pool_t *pool) 136{ 137 if (!qop || strcmp(qop, "auth") == 0) { 138 const char *tmp; 139 unsigned char ha2[APR_MD5_DIGESTSIZE]; 140 apr_status_t status; 141 142 /* calculate ha2: 143 MD5 hash of the combined method and URI */ 144 tmp = apr_psprintf(pool, "%s:%s", 145 method, 146 uri); 147 status = apr_md5(ha2, tmp, strlen(tmp)); 148 if (status) 149 return status; 150 151 *out_ha2 = hex_encode(ha2, pool); 152 153 return APR_SUCCESS; 154 } else { 155 /* TODO: auth-int isn't supported! */ 156 return APR_ENOTIMPL; 157 } 158} 159 160static apr_status_t 161build_auth_header(const char **out_header, 162 digest_authn_info_t *digest_info, 163 const char *path, 164 const char *method, 165 apr_pool_t *pool) 166{ 167 char *hdr; 168 const char *ha2; 169 const char *response; 170 unsigned char response_hdr[APR_MD5_DIGESTSIZE]; 171 const char *response_hdr_hex; 172 apr_status_t status; 173 174 status = build_digest_ha2(&ha2, path, method, digest_info->qop, pool); 175 if (status) 176 return status; 177 178 hdr = apr_psprintf(pool, 179 "Digest realm=\"%s\"," 180 " username=\"%s\"," 181 " nonce=\"%s\"," 182 " uri=\"%s\"", 183 digest_info->realm, digest_info->username, 184 digest_info->nonce, 185 path); 186 187 if (digest_info->qop) { 188 if (! digest_info->cnonce) 189 digest_info->cnonce = random_cnonce(digest_info->pool); 190 191 hdr = apr_psprintf(pool, "%s, nc=%08x, cnonce=\"%s\", qop=\"%s\"", 192 hdr, 193 digest_info->digest_nc, 194 digest_info->cnonce, 195 digest_info->qop); 196 197 /* Build the response header: 198 MD5 hash of the combined HA1 result, server nonce (nonce), 199 request counter (nc), client nonce (cnonce), 200 quality of protection code (qop) and HA2 result. */ 201 response = apr_psprintf(pool, "%s:%s:%08x:%s:%s:%s", 202 digest_info->ha1, digest_info->nonce, 203 digest_info->digest_nc, 204 digest_info->cnonce, digest_info->qop, ha2); 205 } else { 206 /* Build the response header: 207 MD5 hash of the combined HA1 result, server nonce (nonce) 208 and HA2 result. */ 209 response = apr_psprintf(pool, "%s:%s:%s", 210 digest_info->ha1, digest_info->nonce, ha2); 211 } 212 213 status = apr_md5(response_hdr, response, strlen(response)); 214 if (status) 215 return status; 216 217 response_hdr_hex = hex_encode(response_hdr, pool); 218 219 hdr = apr_psprintf(pool, "%s, response=\"%s\"", hdr, response_hdr_hex); 220 221 if (digest_info->opaque) { 222 hdr = apr_psprintf(pool, "%s, opaque=\"%s\"", hdr, 223 digest_info->opaque); 224 } 225 if (digest_info->algorithm) { 226 hdr = apr_psprintf(pool, "%s, algorithm=\"%s\"", hdr, 227 digest_info->algorithm); 228 } 229 230 *out_header = hdr; 231 232 return APR_SUCCESS; 233} 234 235apr_status_t 236serf__handle_digest_auth(int code, 237 serf_request_t *request, 238 serf_bucket_t *response, 239 const char *auth_hdr, 240 const char *auth_attr, 241 void *baton, 242 apr_pool_t *pool) 243{ 244 char *attrs; 245 char *nextkv; 246 const char *realm, *realm_name = NULL; 247 const char *nonce = NULL; 248 const char *algorithm = NULL; 249 const char *qop = NULL; 250 const char *opaque = NULL; 251 const char *key; 252 serf_connection_t *conn = request->conn; 253 serf_context_t *ctx = conn->ctx; 254 serf__authn_info_t *authn_info; 255 digest_authn_info_t *digest_info; 256 apr_status_t status; 257 apr_pool_t *cred_pool; 258 char *username, *password; 259 260 /* Can't do Digest authentication if there's no callback to get 261 username & password. */ 262 if (!ctx->cred_cb) { 263 return SERF_ERROR_AUTHN_FAILED; 264 } 265 266 if (code == 401) { 267 authn_info = serf__get_authn_info_for_server(conn); 268 } else { 269 authn_info = &ctx->proxy_authn_info; 270 } 271 digest_info = authn_info->baton; 272 273 /* Need a copy cuz we're going to write NUL characters into the string. */ 274 attrs = apr_pstrdup(pool, auth_attr); 275 276 /* We're expecting a list of key=value pairs, separated by a comma. 277 Ex. realm="SVN Digest", 278 nonce="f+zTl/leBAA=e371bd3070adfb47b21f5fc64ad8cc21adc371a5", 279 algorithm=MD5, qop="auth" */ 280 for ( ; (key = apr_strtok(attrs, ",", &nextkv)) != NULL; attrs = NULL) { 281 char *val; 282 283 val = strchr(key, '='); 284 if (val == NULL) 285 continue; 286 *val++ = '\0'; 287 288 /* skip leading spaces */ 289 while (*key && *key == ' ') 290 key++; 291 292 /* If the value is quoted, then remove the quotes. */ 293 if (*val == '"') { 294 apr_size_t last = strlen(val) - 1; 295 296 if (val[last] == '"') { 297 val[last] = '\0'; 298 val++; 299 } 300 } 301 302 if (strcmp(key, "realm") == 0) 303 realm_name = val; 304 else if (strcmp(key, "nonce") == 0) 305 nonce = val; 306 else if (strcmp(key, "algorithm") == 0) 307 algorithm = val; 308 else if (strcmp(key, "qop") == 0) 309 qop = val; 310 else if (strcmp(key, "opaque") == 0) 311 opaque = val; 312 313 /* Ignore all unsupported attributes. */ 314 } 315 316 if (!realm_name) { 317 return SERF_ERROR_AUTHN_MISSING_ATTRIBUTE; 318 } 319 320 realm = serf__construct_realm(code == 401 ? HOST : PROXY, 321 conn, realm_name, 322 pool); 323 324 /* Ask the application for credentials */ 325 apr_pool_create(&cred_pool, pool); 326 status = serf__provide_credentials(ctx, 327 &username, &password, 328 request, baton, 329 code, authn_info->scheme->name, 330 realm, cred_pool); 331 if (status) { 332 apr_pool_destroy(cred_pool); 333 return status; 334 } 335 336 digest_info->header = (code == 401) ? "Authorization" : 337 "Proxy-Authorization"; 338 339 /* Store the digest authentication parameters in the context cached for 340 this server in the serf context, so we can use it to create the 341 Authorization header when setting up requests on the same or different 342 connections (e.g. in case of KeepAlive off on the server). 343 TODO: we currently don't cache this info per realm, so each time a request 344 'switches realms', we have to ask the application for new credentials. */ 345 digest_info->pool = conn->pool; 346 digest_info->qop = apr_pstrdup(digest_info->pool, qop); 347 digest_info->nonce = apr_pstrdup(digest_info->pool, nonce); 348 digest_info->cnonce = NULL; 349 digest_info->opaque = apr_pstrdup(digest_info->pool, opaque); 350 digest_info->algorithm = apr_pstrdup(digest_info->pool, algorithm); 351 digest_info->realm = apr_pstrdup(digest_info->pool, realm_name); 352 digest_info->username = apr_pstrdup(digest_info->pool, username); 353 digest_info->digest_nc++; 354 355 status = build_digest_ha1(&digest_info->ha1, username, password, 356 digest_info->realm, digest_info->pool); 357 358 apr_pool_destroy(cred_pool); 359 360 /* If the handshake is finished tell serf it can send as much requests as it 361 likes. */ 362 serf_connection_set_max_outstanding_requests(conn, 0); 363 364 return status; 365} 366 367apr_status_t 368serf__init_digest(int code, 369 serf_context_t *ctx, 370 apr_pool_t *pool) 371{ 372 return APR_SUCCESS; 373} 374 375apr_status_t 376serf__init_digest_connection(const serf__authn_scheme_t *scheme, 377 int code, 378 serf_connection_t *conn, 379 apr_pool_t *pool) 380{ 381 serf_context_t *ctx = conn->ctx; 382 serf__authn_info_t *authn_info; 383 384 if (code == 401) { 385 authn_info = serf__get_authn_info_for_server(conn); 386 } else { 387 authn_info = &ctx->proxy_authn_info; 388 } 389 390 if (!authn_info->baton) { 391 authn_info->baton = apr_pcalloc(pool, sizeof(digest_authn_info_t)); 392 } 393 394 /* Make serf send the initial requests one by one */ 395 serf_connection_set_max_outstanding_requests(conn, 1); 396 397 return APR_SUCCESS; 398} 399 400apr_status_t 401serf__setup_request_digest_auth(peer_t peer, 402 int code, 403 serf_connection_t *conn, 404 serf_request_t *request, 405 const char *method, 406 const char *uri, 407 serf_bucket_t *hdrs_bkt) 408{ 409 serf_context_t *ctx = conn->ctx; 410 serf__authn_info_t *authn_info; 411 digest_authn_info_t *digest_info; 412 apr_status_t status; 413 414 if (peer == HOST) { 415 authn_info = serf__get_authn_info_for_server(conn); 416 } else { 417 authn_info = &ctx->proxy_authn_info; 418 } 419 digest_info = authn_info->baton; 420 421 if (digest_info && digest_info->realm) { 422 const char *value; 423 const char *path; 424 425 /* TODO: per request pool? */ 426 427 /* for request 'CONNECT serf.googlecode.com:443', the uri also should be 428 serf.googlecode.com:443. apr_uri_parse can't handle this, so special 429 case. */ 430 if (strcmp(method, "CONNECT") == 0) 431 path = uri; 432 else { 433 apr_uri_t parsed_uri; 434 435 /* Extract path from uri. */ 436 status = apr_uri_parse(conn->pool, uri, &parsed_uri); 437 if (status) 438 return status; 439 440 path = parsed_uri.path; 441 } 442 443 /* Build a new Authorization header. */ 444 digest_info->header = (peer == HOST) ? "Authorization" : 445 "Proxy-Authorization"; 446 status = build_auth_header(&value, digest_info, path, method, 447 conn->pool); 448 if (status) 449 return status; 450 451 serf_bucket_headers_setn(hdrs_bkt, digest_info->header, 452 value); 453 digest_info->digest_nc++; 454 455 /* Store the uri of this request on the serf_request_t object, to make 456 it available when validating the Authentication-Info header of the 457 matching response. */ 458 request->auth_baton = (void *)path; 459 } 460 461 return APR_SUCCESS; 462} 463 464apr_status_t 465serf__validate_response_digest_auth(const serf__authn_scheme_t *scheme, 466 peer_t peer, 467 int code, 468 serf_connection_t *conn, 469 serf_request_t *request, 470 serf_bucket_t *response, 471 apr_pool_t *pool) 472{ 473 const char *key; 474 char *auth_attr; 475 char *nextkv; 476 const char *rspauth = NULL; 477 const char *qop = NULL; 478 const char *nc_str = NULL; 479 serf_bucket_t *hdrs; 480 serf_context_t *ctx = conn->ctx; 481 apr_status_t status; 482 483 hdrs = serf_bucket_response_get_headers(response); 484 485 /* Need a copy cuz we're going to write NUL characters into the string. */ 486 if (peer == HOST) 487 auth_attr = apr_pstrdup(pool, 488 serf_bucket_headers_get(hdrs, "Authentication-Info")); 489 else 490 auth_attr = apr_pstrdup(pool, 491 serf_bucket_headers_get(hdrs, "Proxy-Authentication-Info")); 492 493 /* If there's no Authentication-Info header there's nothing to validate. */ 494 if (! auth_attr) 495 return APR_SUCCESS; 496 497 /* We're expecting a list of key=value pairs, separated by a comma. 498 Ex. rspauth="8a4b8451084b082be6b105e2b7975087", 499 cnonce="346531653132652d303033392d3435", nc=00000007, 500 qop=auth */ 501 for ( ; (key = apr_strtok(auth_attr, ",", &nextkv)) != NULL; auth_attr = NULL) { 502 char *val; 503 504 val = strchr(key, '='); 505 if (val == NULL) 506 continue; 507 *val++ = '\0'; 508 509 /* skip leading spaces */ 510 while (*key && *key == ' ') 511 key++; 512 513 /* If the value is quoted, then remove the quotes. */ 514 if (*val == '"') { 515 apr_size_t last = strlen(val) - 1; 516 517 if (val[last] == '"') { 518 val[last] = '\0'; 519 val++; 520 } 521 } 522 523 if (strcmp(key, "rspauth") == 0) 524 rspauth = val; 525 else if (strcmp(key, "qop") == 0) 526 qop = val; 527 else if (strcmp(key, "nc") == 0) 528 nc_str = val; 529 } 530 531 if (rspauth) { 532 const char *ha2, *tmp, *resp_hdr_hex; 533 unsigned char resp_hdr[APR_MD5_DIGESTSIZE]; 534 const char *req_uri = request->auth_baton; 535 serf__authn_info_t *authn_info; 536 digest_authn_info_t *digest_info; 537 538 if (peer == HOST) { 539 authn_info = serf__get_authn_info_for_server(conn); 540 } else { 541 authn_info = &ctx->proxy_authn_info; 542 } 543 digest_info = authn_info->baton; 544 545 status = build_digest_ha2(&ha2, req_uri, "", qop, pool); 546 if (status) 547 return status; 548 549 tmp = apr_psprintf(pool, "%s:%s:%s:%s:%s:%s", 550 digest_info->ha1, digest_info->nonce, nc_str, 551 digest_info->cnonce, digest_info->qop, ha2); 552 apr_md5(resp_hdr, tmp, strlen(tmp)); 553 resp_hdr_hex = hex_encode(resp_hdr, pool); 554 555 /* Incorrect response-digest in Authentication-Info header. */ 556 if (strcmp(rspauth, resp_hdr_hex) != 0) { 557 return SERF_ERROR_AUTHN_FAILED; 558 } 559 } 560 561 return APR_SUCCESS; 562} 563