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 *username,
101 const char *password,
102 const char *realm_name,
106 unsigned char ha1[APR_MD5_DIGESTSIZE];
110 MD5 hash of the combined user name, authentication realm and password */
111 tmp = apr_psprintf(pool, "%s:%s:%s",
115 status = apr_md5(ha1, tmp, strlen(tmp));
117 return hex_encode(ha1, pool);
121 build_digest_ha2(const char *uri,
126 if (!qop || strcmp(qop, "auth") == 0) {
128 unsigned char ha2[APR_MD5_DIGESTSIZE];
132 MD5 hash of the combined method and URI */
133 tmp = apr_psprintf(pool, "%s:%s",
136 status = apr_md5(ha2, tmp, strlen(tmp));
138 return hex_encode(ha2, pool);
140 /* TODO: auth-int isn't supported! */
147 build_auth_header(digest_authn_info_t *digest_info,
154 const char *response;
155 unsigned char response_hdr[APR_MD5_DIGESTSIZE];
156 const char *response_hdr_hex;
159 ha2 = build_digest_ha2(path, method, digest_info->qop, pool);
161 hdr = apr_psprintf(pool,
162 "Digest realm=\"%s\","
166 digest_info->realm, digest_info->username,
170 if (digest_info->qop) {
171 if (! digest_info->cnonce)
172 digest_info->cnonce = random_cnonce(digest_info->pool);
174 hdr = apr_psprintf(pool, "%s, nc=%08x, cnonce=\"%s\", qop=\"%s\"",
176 digest_info->digest_nc,
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);
189 /* Build the response header:
190 MD5 hash of the combined HA1 result, server nonce (nonce)
192 response = apr_psprintf(pool, "%s:%s:%s",
193 digest_info->ha1, digest_info->nonce, ha2);
196 status = apr_md5(response_hdr, response, strlen(response));
197 response_hdr_hex = hex_encode(response_hdr, pool);
199 hdr = apr_psprintf(pool, "%s, response=\"%s\"", hdr, response_hdr_hex);
201 if (digest_info->opaque) {
202 hdr = apr_psprintf(pool, "%s, opaque=\"%s\"", hdr,
203 digest_info->opaque);
205 if (digest_info->algorithm) {
206 hdr = apr_psprintf(pool, "%s, algorithm=\"%s\"", hdr,
207 digest_info->algorithm);
214 serf__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,
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;
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;
235 apr_pool_t *cred_pool;
236 char *username, *password;
238 /* Can't do Digest authentication if there's no callback to get
239 username & password. */
241 return SERF_ERROR_AUTHN_FAILED;
245 authn_info = serf__get_authn_info_for_server(conn);
247 authn_info = &ctx->proxy_authn_info;
249 digest_info = authn_info->baton;
251 /* Need a copy cuz we're going to write NUL characters into the string. */
252 attrs = apr_pstrdup(pool, auth_attr);
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) {
261 val = strchr(key, '=');
266 /* skip leading spaces */
267 while (*key && *key == ' ')
270 /* If the value is quoted, then remove the quotes. */
272 apr_size_t last = strlen(val) - 1;
274 if (val[last] == '"') {
280 if (strcmp(key, "realm") == 0)
282 else if (strcmp(key, "nonce") == 0)
284 else if (strcmp(key, "algorithm") == 0)
286 else if (strcmp(key, "qop") == 0)
288 else if (strcmp(key, "opaque") == 0)
291 /* Ignore all unsupported attributes. */
295 return SERF_ERROR_AUTHN_MISSING_ATTRIBUTE;
298 realm = serf__construct_realm(code == 401 ? HOST : PROXY,
302 /* Ask the application for credentials */
303 apr_pool_create(&cred_pool, pool);
304 status = serf__provide_credentials(ctx,
305 &username, &password,
307 code, authn_info->scheme->name,
310 apr_pool_destroy(cred_pool);
314 digest_info->header = (code == 401) ? "Authorization" :
315 "Proxy-Authorization";
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++;
333 digest_info->ha1 = build_digest_ha1(username, password, digest_info->realm,
336 apr_pool_destroy(cred_pool);
338 /* If the handshake is finished tell serf it can send as much requests as it
340 serf_connection_set_max_outstanding_requests(conn, 0);
346 serf__init_digest(int code,
354 serf__init_digest_connection(const serf__authn_scheme_t *scheme,
356 serf_connection_t *conn,
359 serf_context_t *ctx = conn->ctx;
360 serf__authn_info_t *authn_info;
363 authn_info = serf__get_authn_info_for_server(conn);
365 authn_info = &ctx->proxy_authn_info;
368 if (!authn_info->baton) {
369 authn_info->baton = apr_pcalloc(pool, sizeof(digest_authn_info_t));
372 /* Make serf send the initial requests one by one */
373 serf_connection_set_max_outstanding_requests(conn, 1);
379 serf__setup_request_digest_auth(peer_t peer,
381 serf_connection_t *conn,
382 serf_request_t *request,
385 serf_bucket_t *hdrs_bkt)
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;
393 authn_info = serf__get_authn_info_for_server(conn);
395 authn_info = &ctx->proxy_authn_info;
397 digest_info = authn_info->baton;
399 if (digest_info && digest_info->realm) {
403 /* TODO: per request pool? */
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
408 if (strcmp(method, "CONNECT") == 0)
411 apr_uri_t parsed_uri;
413 /* Extract path from uri. */
414 status = apr_uri_parse(conn->pool, uri, &parsed_uri);
418 path = parsed_uri.path;
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,
427 serf_bucket_headers_setn(hdrs_bkt, digest_info->header,
429 digest_info->digest_nc++;
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;
441 serf__validate_response_digest_auth(peer_t peer,
443 serf_connection_t *conn,
444 serf_request_t *request,
445 serf_bucket_t *response,
451 const char *rspauth = NULL;
452 const char *qop = NULL;
453 const char *nc_str = NULL;
455 serf_context_t *ctx = conn->ctx;
457 hdrs = serf_bucket_response_get_headers(response);
459 /* Need a copy cuz we're going to write NUL characters into the string. */
461 auth_attr = apr_pstrdup(pool,
462 serf_bucket_headers_get(hdrs, "Authentication-Info"));
464 auth_attr = apr_pstrdup(pool,
465 serf_bucket_headers_get(hdrs, "Proxy-Authentication-Info"));
467 /* If there's no Authentication-Info header there's nothing to validate. */
471 /* We're expecting a list of key=value pairs, separated by a comma.
472 Ex. rspauth="8a4b8451084b082be6b105e2b7975087",
473 cnonce="346531653132652d303033392d3435", nc=00000007,
475 for ( ; (key = apr_strtok(auth_attr, ",", &nextkv)) != NULL; auth_attr = NULL) {
478 val = strchr(key, '=');
483 /* skip leading spaces */
484 while (*key && *key == ' ')
487 /* If the value is quoted, then remove the quotes. */
489 apr_size_t last = strlen(val) - 1;
491 if (val[last] == '"') {
497 if (strcmp(key, "rspauth") == 0)
499 else if (strcmp(key, "qop") == 0)
501 else if (strcmp(key, "nc") == 0)
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;
513 authn_info = serf__get_authn_info_for_server(conn);
515 authn_info = &ctx->proxy_authn_info;
517 digest_info = authn_info->baton;
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);
526 /* Incorrect response-digest in Authentication-Info header. */
527 if (strcmp(rspauth, resp_hdr_hex) != 0) {
528 return SERF_ERROR_AUTHN_FAILED;