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
10 * http://www.apache.org/licenses/LICENSE-2.0
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
18 * ====================================================================
21 /*** Digest authentication ***/
24 #include <serf_private.h>
25 #include <auth/auth.h>
28 #include <apr_base64.h>
29 #include <apr_strings.h>
33 /** Digest authentication, implements RFC 2617. **/
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. */
39 /* Stores the context information related to Digest authentication.
40 This information is stored in the per server cache in the serf context. */
41 typedef struct digest_authn_info_t {
42 /* nonce-count for digest authentication */
43 unsigned int digest_nc;
53 const char *algorithm;
58 } digest_authn_info_t;
63 return (v < 10) ? '0' + v : 'a' + (v - 10);
67 * Convert a string if ASCII characters HASHVAL to its hexadecimal
70 * The returned string will be allocated in the POOL and be null-terminated.
73 hex_encode(const unsigned char *hashval,
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);
82 hexval[APR_MD5_DIGESTSIZE * 2] = '\0';
87 * Returns a 36-byte long string of random characters.
88 * UUIDs are formatted as: 00112233-4455-6677-8899-AABBCCDDEEFF.
90 * The returned string will be allocated in the POOL and be null-terminated.
93 random_cnonce(apr_pool_t *pool)
96 char *buf = apr_palloc(pool, APR_UUID_FORMATTED_LENGTH + 1);
99 apr_uuid_format(buf, &uuid);
101 return hex_encode((unsigned char*)buf, pool);
105 build_digest_ha1(const char **out_ha1,
106 const char *username,
107 const char *password,
108 const char *realm_name,
112 unsigned char ha1[APR_MD5_DIGESTSIZE];
116 MD5 hash of the combined user name, authentication realm and password */
117 tmp = apr_psprintf(pool, "%s:%s:%s",
121 status = apr_md5(ha1, tmp, strlen(tmp));
125 *out_ha1 = hex_encode(ha1, pool);
131 build_digest_ha2(const char **out_ha2,
137 if (!qop || strcmp(qop, "auth") == 0) {
139 unsigned char ha2[APR_MD5_DIGESTSIZE];
143 MD5 hash of the combined method and URI */
144 tmp = apr_psprintf(pool, "%s:%s",
147 status = apr_md5(ha2, tmp, strlen(tmp));
151 *out_ha2 = hex_encode(ha2, pool);
155 /* TODO: auth-int isn't supported! */
161 build_auth_header(const char **out_header,
162 digest_authn_info_t *digest_info,
169 const char *response;
170 unsigned char response_hdr[APR_MD5_DIGESTSIZE];
171 const char *response_hdr_hex;
174 status = build_digest_ha2(&ha2, path, method, digest_info->qop, pool);
178 hdr = apr_psprintf(pool,
179 "Digest realm=\"%s\","
183 digest_info->realm, digest_info->username,
187 if (digest_info->qop) {
188 if (! digest_info->cnonce)
189 digest_info->cnonce = random_cnonce(digest_info->pool);
191 hdr = apr_psprintf(pool, "%s, nc=%08x, cnonce=\"%s\", qop=\"%s\"",
193 digest_info->digest_nc,
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);
206 /* Build the response header:
207 MD5 hash of the combined HA1 result, server nonce (nonce)
209 response = apr_psprintf(pool, "%s:%s:%s",
210 digest_info->ha1, digest_info->nonce, ha2);
213 status = apr_md5(response_hdr, response, strlen(response));
217 response_hdr_hex = hex_encode(response_hdr, pool);
219 hdr = apr_psprintf(pool, "%s, response=\"%s\"", hdr, response_hdr_hex);
221 if (digest_info->opaque) {
222 hdr = apr_psprintf(pool, "%s, opaque=\"%s\"", hdr,
223 digest_info->opaque);
225 if (digest_info->algorithm) {
226 hdr = apr_psprintf(pool, "%s, algorithm=\"%s\"", hdr,
227 digest_info->algorithm);
236 serf__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,
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;
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;
257 apr_pool_t *cred_pool;
258 char *username, *password;
260 /* Can't do Digest authentication if there's no callback to get
261 username & password. */
263 return SERF_ERROR_AUTHN_FAILED;
267 authn_info = serf__get_authn_info_for_server(conn);
269 authn_info = &ctx->proxy_authn_info;
271 digest_info = authn_info->baton;
273 /* Need a copy cuz we're going to write NUL characters into the string. */
274 attrs = apr_pstrdup(pool, auth_attr);
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) {
283 val = strchr(key, '=');
288 /* skip leading spaces */
289 while (*key && *key == ' ')
292 /* If the value is quoted, then remove the quotes. */
294 apr_size_t last = strlen(val) - 1;
296 if (val[last] == '"') {
302 if (strcmp(key, "realm") == 0)
304 else if (strcmp(key, "nonce") == 0)
306 else if (strcmp(key, "algorithm") == 0)
308 else if (strcmp(key, "qop") == 0)
310 else if (strcmp(key, "opaque") == 0)
313 /* Ignore all unsupported attributes. */
317 return SERF_ERROR_AUTHN_MISSING_ATTRIBUTE;
320 realm = serf__construct_realm(code == 401 ? HOST : PROXY,
324 /* Ask the application for credentials */
325 apr_pool_create(&cred_pool, pool);
326 status = serf__provide_credentials(ctx,
327 &username, &password,
329 code, authn_info->scheme->name,
332 apr_pool_destroy(cred_pool);
336 digest_info->header = (code == 401) ? "Authorization" :
337 "Proxy-Authorization";
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++;
355 status = build_digest_ha1(&digest_info->ha1, username, password,
356 digest_info->realm, digest_info->pool);
358 apr_pool_destroy(cred_pool);
360 /* If the handshake is finished tell serf it can send as much requests as it
362 serf_connection_set_max_outstanding_requests(conn, 0);
368 serf__init_digest(int code,
376 serf__init_digest_connection(const serf__authn_scheme_t *scheme,
378 serf_connection_t *conn,
381 serf_context_t *ctx = conn->ctx;
382 serf__authn_info_t *authn_info;
385 authn_info = serf__get_authn_info_for_server(conn);
387 authn_info = &ctx->proxy_authn_info;
390 if (!authn_info->baton) {
391 authn_info->baton = apr_pcalloc(pool, sizeof(digest_authn_info_t));
394 /* Make serf send the initial requests one by one */
395 serf_connection_set_max_outstanding_requests(conn, 1);
401 serf__setup_request_digest_auth(peer_t peer,
403 serf_connection_t *conn,
404 serf_request_t *request,
407 serf_bucket_t *hdrs_bkt)
409 serf_context_t *ctx = conn->ctx;
410 serf__authn_info_t *authn_info;
411 digest_authn_info_t *digest_info;
415 authn_info = serf__get_authn_info_for_server(conn);
417 authn_info = &ctx->proxy_authn_info;
419 digest_info = authn_info->baton;
421 if (digest_info && digest_info->realm) {
425 /* TODO: per request pool? */
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
430 if (strcmp(method, "CONNECT") == 0)
433 apr_uri_t parsed_uri;
435 /* Extract path from uri. */
436 status = apr_uri_parse(conn->pool, uri, &parsed_uri);
440 path = parsed_uri.path;
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,
451 serf_bucket_headers_setn(hdrs_bkt, digest_info->header,
453 digest_info->digest_nc++;
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;
465 serf__validate_response_digest_auth(const serf__authn_scheme_t *scheme,
468 serf_connection_t *conn,
469 serf_request_t *request,
470 serf_bucket_t *response,
476 const char *rspauth = NULL;
477 const char *qop = NULL;
478 const char *nc_str = NULL;
480 serf_context_t *ctx = conn->ctx;
483 hdrs = serf_bucket_response_get_headers(response);
485 /* Need a copy cuz we're going to write NUL characters into the string. */
487 auth_attr = apr_pstrdup(pool,
488 serf_bucket_headers_get(hdrs, "Authentication-Info"));
490 auth_attr = apr_pstrdup(pool,
491 serf_bucket_headers_get(hdrs, "Proxy-Authentication-Info"));
493 /* If there's no Authentication-Info header there's nothing to validate. */
497 /* We're expecting a list of key=value pairs, separated by a comma.
498 Ex. rspauth="8a4b8451084b082be6b105e2b7975087",
499 cnonce="346531653132652d303033392d3435", nc=00000007,
501 for ( ; (key = apr_strtok(auth_attr, ",", &nextkv)) != NULL; auth_attr = NULL) {
504 val = strchr(key, '=');
509 /* skip leading spaces */
510 while (*key && *key == ' ')
513 /* If the value is quoted, then remove the quotes. */
515 apr_size_t last = strlen(val) - 1;
517 if (val[last] == '"') {
523 if (strcmp(key, "rspauth") == 0)
525 else if (strcmp(key, "qop") == 0)
527 else if (strcmp(key, "nc") == 0)
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;
539 authn_info = serf__get_authn_info_for_server(conn);
541 authn_info = &ctx->proxy_authn_info;
543 digest_info = authn_info->baton;
545 status = build_digest_ha2(&ha2, req_uri, "", qop, pool);
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);
555 /* Incorrect response-digest in Authentication-Info header. */
556 if (strcmp(rspauth, resp_hdr_hex) != 0) {
557 return SERF_ERROR_AUTHN_FAILED;