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