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 const char * 100build_digest_ha1(const char *username, 101 const char *password, 102 const char *realm_name, 103 apr_pool_t *pool) 104{ 105 const char *tmp; 106 unsigned char ha1[APR_MD5_DIGESTSIZE]; 107 apr_status_t status; 108 109 /* calculate ha1: 110 MD5 hash of the combined user name, authentication realm and password */ 111 tmp = apr_psprintf(pool, "%s:%s:%s", 112 username, 113 realm_name, 114 password); 115 status = apr_md5(ha1, tmp, strlen(tmp)); 116 117 return hex_encode(ha1, pool); 118} 119 120static const char * 121build_digest_ha2(const char *uri, 122 const char *method, 123 const char *qop, 124 apr_pool_t *pool) 125{ 126 if (!qop || strcmp(qop, "auth") == 0) { 127 const char *tmp; 128 unsigned char ha2[APR_MD5_DIGESTSIZE]; 129 apr_status_t status; 130 131 /* calculate ha2: 132 MD5 hash of the combined method and URI */ 133 tmp = apr_psprintf(pool, "%s:%s", 134 method, 135 uri); 136 status = apr_md5(ha2, tmp, strlen(tmp)); 137 138 return hex_encode(ha2, pool); 139 } else { 140 /* TODO: auth-int isn't supported! */ 141 } 142 143 return NULL; 144} 145 146static const char * 147build_auth_header(digest_authn_info_t *digest_info, 148 const char *path, 149 const char *method, 150 apr_pool_t *pool) 151{ 152 char *hdr; 153 const char *ha2; 154 const char *response; 155 unsigned char response_hdr[APR_MD5_DIGESTSIZE]; 156 const char *response_hdr_hex; 157 apr_status_t status; 158 159 ha2 = build_digest_ha2(path, method, digest_info->qop, pool); 160 161 hdr = apr_psprintf(pool, 162 "Digest realm=\"%s\"," 163 " username=\"%s\"," 164 " nonce=\"%s\"," 165 " uri=\"%s\"", 166 digest_info->realm, digest_info->username, 167 digest_info->nonce, 168 path); 169 170 if (digest_info->qop) { 171 if (! digest_info->cnonce) 172 digest_info->cnonce = random_cnonce(digest_info->pool); 173 174 hdr = apr_psprintf(pool, "%s, nc=%08x, cnonce=\"%s\", qop=\"%s\"", 175 hdr, 176 digest_info->digest_nc, 177 digest_info->cnonce, 178 digest_info->qop); 179 180 /* Build the response header: 181 MD5 hash of the combined HA1 result, server nonce (nonce), 182 request counter (nc), client nonce (cnonce), 183 quality of protection code (qop) and HA2 result. */ 184 response = apr_psprintf(pool, "%s:%s:%08x:%s:%s:%s", 185 digest_info->ha1, digest_info->nonce, 186 digest_info->digest_nc, 187 digest_info->cnonce, digest_info->qop, ha2); 188 } else { 189 /* Build the response header: 190 MD5 hash of the combined HA1 result, server nonce (nonce) 191 and HA2 result. */ 192 response = apr_psprintf(pool, "%s:%s:%s", 193 digest_info->ha1, digest_info->nonce, ha2); 194 } 195 196 status = apr_md5(response_hdr, response, strlen(response)); 197 response_hdr_hex = hex_encode(response_hdr, pool); 198 199 hdr = apr_psprintf(pool, "%s, response=\"%s\"", hdr, response_hdr_hex); 200 201 if (digest_info->opaque) { 202 hdr = apr_psprintf(pool, "%s, opaque=\"%s\"", hdr, 203 digest_info->opaque); 204 } 205 if (digest_info->algorithm) { 206 hdr = apr_psprintf(pool, "%s, algorithm=\"%s\"", hdr, 207 digest_info->algorithm); 208 } 209 210 return hdr; 211} 212 213apr_status_t 214serf__handle_digest_auth(int code, 215 serf_request_t *request, 216 serf_bucket_t *response, 217 const char *auth_hdr, 218 const char *auth_attr, 219 void *baton, 220 apr_pool_t *pool) 221{ 222 char *attrs; 223 char *nextkv; 224 const char *realm, *realm_name = NULL; 225 const char *nonce = NULL; 226 const char *algorithm = NULL; 227 const char *qop = NULL; 228 const char *opaque = NULL; 229 const char *key; 230 serf_connection_t *conn = request->conn; 231 serf_context_t *ctx = conn->ctx; 232 serf__authn_info_t *authn_info; 233 digest_authn_info_t *digest_info; 234 apr_status_t status; 235 apr_pool_t *cred_pool; 236 char *username, *password; 237 238 /* Can't do Digest authentication if there's no callback to get 239 username & password. */ 240 if (!ctx->cred_cb) { 241 return SERF_ERROR_AUTHN_FAILED; 242 } 243 244 if (code == 401) { 245 authn_info = serf__get_authn_info_for_server(conn); 246 } else { 247 authn_info = &ctx->proxy_authn_info; 248 } 249 digest_info = authn_info->baton; 250 251 /* Need a copy cuz we're going to write NUL characters into the string. */ 252 attrs = apr_pstrdup(pool, auth_attr); 253 254 /* We're expecting a list of key=value pairs, separated by a comma. 255 Ex. realm="SVN Digest", 256 nonce="f+zTl/leBAA=e371bd3070adfb47b21f5fc64ad8cc21adc371a5", 257 algorithm=MD5, qop="auth" */ 258 for ( ; (key = apr_strtok(attrs, ",", &nextkv)) != NULL; attrs = NULL) { 259 char *val; 260 261 val = strchr(key, '='); 262 if (val == NULL) 263 continue; 264 *val++ = '\0'; 265 266 /* skip leading spaces */ 267 while (*key && *key == ' ') 268 key++; 269 270 /* If the value is quoted, then remove the quotes. */ 271 if (*val == '"') { 272 apr_size_t last = strlen(val) - 1; 273 274 if (val[last] == '"') { 275 val[last] = '\0'; 276 val++; 277 } 278 } 279 280 if (strcmp(key, "realm") == 0) 281 realm_name = val; 282 else if (strcmp(key, "nonce") == 0) 283 nonce = val; 284 else if (strcmp(key, "algorithm") == 0) 285 algorithm = val; 286 else if (strcmp(key, "qop") == 0) 287 qop = val; 288 else if (strcmp(key, "opaque") == 0) 289 opaque = val; 290 291 /* Ignore all unsupported attributes. */ 292 } 293 294 if (!realm_name) { 295 return SERF_ERROR_AUTHN_MISSING_ATTRIBUTE; 296 } 297 298 realm = serf__construct_realm(code == 401 ? HOST : PROXY, 299 conn, realm_name, 300 pool); 301 302 /* Ask the application for credentials */ 303 apr_pool_create(&cred_pool, pool); 304 status = serf__provide_credentials(ctx, 305 &username, &password, 306 request, baton, 307 code, authn_info->scheme->name, 308 realm, cred_pool); 309 if (status) { 310 apr_pool_destroy(cred_pool); 311 return status; 312 } 313 314 digest_info->header = (code == 401) ? "Authorization" : 315 "Proxy-Authorization"; 316 317 /* Store the digest authentication parameters in the context cached for 318 this server in the serf context, so we can use it to create the 319 Authorization header when setting up requests on the same or different 320 connections (e.g. in case of KeepAlive off on the server). 321 TODO: we currently don't cache this info per realm, so each time a request 322 'switches realms', we have to ask the application for new credentials. */ 323 digest_info->pool = conn->pool; 324 digest_info->qop = apr_pstrdup(digest_info->pool, qop); 325 digest_info->nonce = apr_pstrdup(digest_info->pool, nonce); 326 digest_info->cnonce = NULL; 327 digest_info->opaque = apr_pstrdup(digest_info->pool, opaque); 328 digest_info->algorithm = apr_pstrdup(digest_info->pool, algorithm); 329 digest_info->realm = apr_pstrdup(digest_info->pool, realm_name); 330 digest_info->username = apr_pstrdup(digest_info->pool, username); 331 digest_info->digest_nc++; 332 333 digest_info->ha1 = build_digest_ha1(username, password, digest_info->realm, 334 digest_info->pool); 335 336 apr_pool_destroy(cred_pool); 337 338 /* If the handshake is finished tell serf it can send as much requests as it 339 likes. */ 340 serf_connection_set_max_outstanding_requests(conn, 0); 341 342 return APR_SUCCESS; 343} 344 345apr_status_t 346serf__init_digest(int code, 347 serf_context_t *ctx, 348 apr_pool_t *pool) 349{ 350 return APR_SUCCESS; 351} 352 353apr_status_t 354serf__init_digest_connection(const serf__authn_scheme_t *scheme, 355 int code, 356 serf_connection_t *conn, 357 apr_pool_t *pool) 358{ 359 serf_context_t *ctx = conn->ctx; 360 serf__authn_info_t *authn_info; 361 362 if (code == 401) { 363 authn_info = serf__get_authn_info_for_server(conn); 364 } else { 365 authn_info = &ctx->proxy_authn_info; 366 } 367 368 if (!authn_info->baton) { 369 authn_info->baton = apr_pcalloc(pool, sizeof(digest_authn_info_t)); 370 } 371 372 /* Make serf send the initial requests one by one */ 373 serf_connection_set_max_outstanding_requests(conn, 1); 374 375 return APR_SUCCESS; 376} 377 378apr_status_t 379serf__setup_request_digest_auth(peer_t peer, 380 int code, 381 serf_connection_t *conn, 382 serf_request_t *request, 383 const char *method, 384 const char *uri, 385 serf_bucket_t *hdrs_bkt) 386{ 387 serf_context_t *ctx = conn->ctx; 388 serf__authn_info_t *authn_info; 389 digest_authn_info_t *digest_info; 390 apr_status_t status = APR_SUCCESS; 391 392 if (peer == HOST) { 393 authn_info = serf__get_authn_info_for_server(conn); 394 } else { 395 authn_info = &ctx->proxy_authn_info; 396 } 397 digest_info = authn_info->baton; 398 399 if (digest_info && digest_info->realm) { 400 const char *value; 401 const char *path; 402 403 /* TODO: per request pool? */ 404 405 /* for request 'CONNECT serf.googlecode.com:443', the uri also should be 406 serf.googlecode.com:443. apr_uri_parse can't handle this, so special 407 case. */ 408 if (strcmp(method, "CONNECT") == 0) 409 path = uri; 410 else { 411 apr_uri_t parsed_uri; 412 413 /* Extract path from uri. */ 414 status = apr_uri_parse(conn->pool, uri, &parsed_uri); 415 if (status) 416 return status; 417 418 path = parsed_uri.path; 419 } 420 421 /* Build a new Authorization header. */ 422 digest_info->header = (peer == HOST) ? "Authorization" : 423 "Proxy-Authorization"; 424 value = build_auth_header(digest_info, path, method, 425 conn->pool); 426 427 serf_bucket_headers_setn(hdrs_bkt, digest_info->header, 428 value); 429 digest_info->digest_nc++; 430 431 /* Store the uri of this request on the serf_request_t object, to make 432 it available when validating the Authentication-Info header of the 433 matching response. */ 434 request->auth_baton = path; 435 } 436 437 return status; 438} 439 440apr_status_t 441serf__validate_response_digest_auth(peer_t peer, 442 int code, 443 serf_connection_t *conn, 444 serf_request_t *request, 445 serf_bucket_t *response, 446 apr_pool_t *pool) 447{ 448 const char *key; 449 char *auth_attr; 450 char *nextkv; 451 const char *rspauth = NULL; 452 const char *qop = NULL; 453 const char *nc_str = NULL; 454 serf_bucket_t *hdrs; 455 serf_context_t *ctx = conn->ctx; 456 457 hdrs = serf_bucket_response_get_headers(response); 458 459 /* Need a copy cuz we're going to write NUL characters into the string. */ 460 if (peer == HOST) 461 auth_attr = apr_pstrdup(pool, 462 serf_bucket_headers_get(hdrs, "Authentication-Info")); 463 else 464 auth_attr = apr_pstrdup(pool, 465 serf_bucket_headers_get(hdrs, "Proxy-Authentication-Info")); 466 467 /* If there's no Authentication-Info header there's nothing to validate. */ 468 if (! auth_attr) 469 return APR_SUCCESS; 470 471 /* We're expecting a list of key=value pairs, separated by a comma. 472 Ex. rspauth="8a4b8451084b082be6b105e2b7975087", 473 cnonce="346531653132652d303033392d3435", nc=00000007, 474 qop=auth */ 475 for ( ; (key = apr_strtok(auth_attr, ",", &nextkv)) != NULL; auth_attr = NULL) { 476 char *val; 477 478 val = strchr(key, '='); 479 if (val == NULL) 480 continue; 481 *val++ = '\0'; 482 483 /* skip leading spaces */ 484 while (*key && *key == ' ') 485 key++; 486 487 /* If the value is quoted, then remove the quotes. */ 488 if (*val == '"') { 489 apr_size_t last = strlen(val) - 1; 490 491 if (val[last] == '"') { 492 val[last] = '\0'; 493 val++; 494 } 495 } 496 497 if (strcmp(key, "rspauth") == 0) 498 rspauth = val; 499 else if (strcmp(key, "qop") == 0) 500 qop = val; 501 else if (strcmp(key, "nc") == 0) 502 nc_str = val; 503 } 504 505 if (rspauth) { 506 const char *ha2, *tmp, *resp_hdr_hex; 507 unsigned char resp_hdr[APR_MD5_DIGESTSIZE]; 508 const char *req_uri = request->auth_baton; 509 serf__authn_info_t *authn_info; 510 digest_authn_info_t *digest_info; 511 512 if (peer == HOST) { 513 authn_info = serf__get_authn_info_for_server(conn); 514 } else { 515 authn_info = &ctx->proxy_authn_info; 516 } 517 digest_info = authn_info->baton; 518 519 ha2 = build_digest_ha2(req_uri, "", qop, pool); 520 tmp = apr_psprintf(pool, "%s:%s:%s:%s:%s:%s", 521 digest_info->ha1, digest_info->nonce, nc_str, 522 digest_info->cnonce, digest_info->qop, ha2); 523 apr_md5(resp_hdr, tmp, strlen(tmp)); 524 resp_hdr_hex = hex_encode(resp_hdr, pool); 525 526 /* Incorrect response-digest in Authentication-Info header. */ 527 if (strcmp(rspauth, resp_hdr_hex) != 0) { 528 return SERF_ERROR_AUTHN_FAILED; 529 } 530 } 531 532 return APR_SUCCESS; 533} 534