1 /* Copyright 2009 Justin Erenkrantz and Greg Stein
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
7 * http://www.apache.org/licenses/LICENSE-2.0
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.
16 /*** Digest authentication ***/
19 #include <serf_private.h>
20 #include <auth/auth.h>
23 #include <apr_base64.h>
24 #include <apr_strings.h>
28 /** Digest authentication, implements RFC 2617. **/
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. */
34 /* Stores the context information related to Digest authentication.
35 This information is stored in the per server cache in the serf context. */
36 typedef struct digest_authn_info_t {
37 /* nonce-count for digest authentication */
38 unsigned int digest_nc;
48 const char *algorithm;
53 } digest_authn_info_t;
58 return (v < 10) ? '0' + v : 'a' + (v - 10);
62 * Convert a string if ASCII characters HASHVAL to its hexadecimal
65 * The returned string will be allocated in the POOL and be null-terminated.
68 hex_encode(const unsigned char *hashval,
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);
77 hexval[APR_MD5_DIGESTSIZE * 2] = '\0';
82 * Returns a 36-byte long string of random characters.
83 * UUIDs are formatted as: 00112233-4455-6677-8899-AABBCCDDEEFF.
85 * The returned string will be allocated in the POOL and be null-terminated.
88 random_cnonce(apr_pool_t *pool)
91 char *buf = apr_palloc(pool, APR_UUID_FORMATTED_LENGTH + 1);
94 apr_uuid_format(buf, &uuid);
96 return hex_encode((unsigned char*)buf, pool);
100 build_digest_ha1(const char **out_ha1,
101 const char *username,
102 const char *password,
103 const char *realm_name,
107 unsigned char ha1[APR_MD5_DIGESTSIZE];
111 MD5 hash of the combined user name, authentication realm and password */
112 tmp = apr_psprintf(pool, "%s:%s:%s",
116 status = apr_md5(ha1, tmp, strlen(tmp));
120 *out_ha1 = hex_encode(ha1, pool);
126 build_digest_ha2(const char **out_ha2,
132 if (!qop || strcmp(qop, "auth") == 0) {
134 unsigned char ha2[APR_MD5_DIGESTSIZE];
138 MD5 hash of the combined method and URI */
139 tmp = apr_psprintf(pool, "%s:%s",
142 status = apr_md5(ha2, tmp, strlen(tmp));
146 *out_ha2 = hex_encode(ha2, pool);
150 /* TODO: auth-int isn't supported! */
156 build_auth_header(const char **out_header,
157 digest_authn_info_t *digest_info,
164 const char *response;
165 unsigned char response_hdr[APR_MD5_DIGESTSIZE];
166 const char *response_hdr_hex;
169 status = build_digest_ha2(&ha2, path, method, digest_info->qop, pool);
173 hdr = apr_psprintf(pool,
174 "Digest realm=\"%s\","
178 digest_info->realm, digest_info->username,
182 if (digest_info->qop) {
183 if (! digest_info->cnonce)
184 digest_info->cnonce = random_cnonce(digest_info->pool);
186 hdr = apr_psprintf(pool, "%s, nc=%08x, cnonce=\"%s\", qop=\"%s\"",
188 digest_info->digest_nc,
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);
201 /* Build the response header:
202 MD5 hash of the combined HA1 result, server nonce (nonce)
204 response = apr_psprintf(pool, "%s:%s:%s",
205 digest_info->ha1, digest_info->nonce, ha2);
208 status = apr_md5(response_hdr, response, strlen(response));
212 response_hdr_hex = hex_encode(response_hdr, pool);
214 hdr = apr_psprintf(pool, "%s, response=\"%s\"", hdr, response_hdr_hex);
216 if (digest_info->opaque) {
217 hdr = apr_psprintf(pool, "%s, opaque=\"%s\"", hdr,
218 digest_info->opaque);
220 if (digest_info->algorithm) {
221 hdr = apr_psprintf(pool, "%s, algorithm=\"%s\"", hdr,
222 digest_info->algorithm);
231 serf__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,
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;
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;
252 apr_pool_t *cred_pool;
253 char *username, *password;
255 /* Can't do Digest authentication if there's no callback to get
256 username & password. */
258 return SERF_ERROR_AUTHN_FAILED;
262 authn_info = serf__get_authn_info_for_server(conn);
264 authn_info = &ctx->proxy_authn_info;
266 digest_info = authn_info->baton;
268 /* Need a copy cuz we're going to write NUL characters into the string. */
269 attrs = apr_pstrdup(pool, auth_attr);
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) {
278 val = strchr(key, '=');
283 /* skip leading spaces */
284 while (*key && *key == ' ')
287 /* If the value is quoted, then remove the quotes. */
289 apr_size_t last = strlen(val) - 1;
291 if (val[last] == '"') {
297 if (strcmp(key, "realm") == 0)
299 else if (strcmp(key, "nonce") == 0)
301 else if (strcmp(key, "algorithm") == 0)
303 else if (strcmp(key, "qop") == 0)
305 else if (strcmp(key, "opaque") == 0)
308 /* Ignore all unsupported attributes. */
312 return SERF_ERROR_AUTHN_MISSING_ATTRIBUTE;
315 realm = serf__construct_realm(code == 401 ? HOST : PROXY,
319 /* Ask the application for credentials */
320 apr_pool_create(&cred_pool, pool);
321 status = serf__provide_credentials(ctx,
322 &username, &password,
324 code, authn_info->scheme->name,
327 apr_pool_destroy(cred_pool);
331 digest_info->header = (code == 401) ? "Authorization" :
332 "Proxy-Authorization";
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++;
350 status = build_digest_ha1(&digest_info->ha1, username, password,
351 digest_info->realm, digest_info->pool);
353 apr_pool_destroy(cred_pool);
355 /* If the handshake is finished tell serf it can send as much requests as it
357 serf_connection_set_max_outstanding_requests(conn, 0);
363 serf__init_digest(int code,
371 serf__init_digest_connection(const serf__authn_scheme_t *scheme,
373 serf_connection_t *conn,
376 serf_context_t *ctx = conn->ctx;
377 serf__authn_info_t *authn_info;
380 authn_info = serf__get_authn_info_for_server(conn);
382 authn_info = &ctx->proxy_authn_info;
385 if (!authn_info->baton) {
386 authn_info->baton = apr_pcalloc(pool, sizeof(digest_authn_info_t));
389 /* Make serf send the initial requests one by one */
390 serf_connection_set_max_outstanding_requests(conn, 1);
396 serf__setup_request_digest_auth(peer_t peer,
398 serf_connection_t *conn,
399 serf_request_t *request,
402 serf_bucket_t *hdrs_bkt)
404 serf_context_t *ctx = conn->ctx;
405 serf__authn_info_t *authn_info;
406 digest_authn_info_t *digest_info;
410 authn_info = serf__get_authn_info_for_server(conn);
412 authn_info = &ctx->proxy_authn_info;
414 digest_info = authn_info->baton;
416 if (digest_info && digest_info->realm) {
420 /* TODO: per request pool? */
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
425 if (strcmp(method, "CONNECT") == 0)
428 apr_uri_t parsed_uri;
430 /* Extract path from uri. */
431 status = apr_uri_parse(conn->pool, uri, &parsed_uri);
435 path = parsed_uri.path;
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,
446 serf_bucket_headers_setn(hdrs_bkt, digest_info->header,
448 digest_info->digest_nc++;
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;
460 serf__validate_response_digest_auth(const serf__authn_scheme_t *scheme,
463 serf_connection_t *conn,
464 serf_request_t *request,
465 serf_bucket_t *response,
471 const char *rspauth = NULL;
472 const char *qop = NULL;
473 const char *nc_str = NULL;
475 serf_context_t *ctx = conn->ctx;
478 hdrs = serf_bucket_response_get_headers(response);
480 /* Need a copy cuz we're going to write NUL characters into the string. */
482 auth_attr = apr_pstrdup(pool,
483 serf_bucket_headers_get(hdrs, "Authentication-Info"));
485 auth_attr = apr_pstrdup(pool,
486 serf_bucket_headers_get(hdrs, "Proxy-Authentication-Info"));
488 /* If there's no Authentication-Info header there's nothing to validate. */
492 /* We're expecting a list of key=value pairs, separated by a comma.
493 Ex. rspauth="8a4b8451084b082be6b105e2b7975087",
494 cnonce="346531653132652d303033392d3435", nc=00000007,
496 for ( ; (key = apr_strtok(auth_attr, ",", &nextkv)) != NULL; auth_attr = NULL) {
499 val = strchr(key, '=');
504 /* skip leading spaces */
505 while (*key && *key == ' ')
508 /* If the value is quoted, then remove the quotes. */
510 apr_size_t last = strlen(val) - 1;
512 if (val[last] == '"') {
518 if (strcmp(key, "rspauth") == 0)
520 else if (strcmp(key, "qop") == 0)
522 else if (strcmp(key, "nc") == 0)
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;
534 authn_info = serf__get_authn_info_for_server(conn);
536 authn_info = &ctx->proxy_authn_info;
538 digest_info = authn_info->baton;
540 status = build_digest_ha2(&ha2, req_uri, "", qop, pool);
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);
550 /* Incorrect response-digest in Authentication-Info header. */
551 if (strcmp(rspauth, resp_hdr_hex) != 0) {
552 return SERF_ERROR_AUTHN_FAILED;