auth_digest.c revision 362181
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
9 *
10 *      http://www.apache.org/licenses/LICENSE-2.0
11 *
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
17 *    under the License.
18 * ====================================================================
19 */
20
21/*** Digest authentication ***/
22
23#include <serf.h>
24#include <serf_private.h>
25#include <auth/auth.h>
26
27#include <apr.h>
28#include <apr_base64.h>
29#include <apr_strings.h>
30#include <apr_uuid.h>
31#include <apr_md5.h>
32
33/** Digest authentication, implements RFC 2617. **/
34
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. */
38
39/* Stores the context information related to Digest authentication.
40   This information is stored in the per server cache in the serf context. */
41typedef struct digest_authn_info_t {
42    /* nonce-count for digest authentication */
43    unsigned int digest_nc;
44
45    const char *header;
46
47    const char *ha1;
48
49    const char *realm;
50    const char *cnonce;
51    const char *nonce;
52    const char *opaque;
53    const char *algorithm;
54    const char *qop;
55    const char *username;
56
57    apr_pool_t *pool;
58} digest_authn_info_t;
59
60static char
61int_to_hex(int v)
62{
63    return (v < 10) ? '0' + v : 'a' + (v - 10);
64}
65
66/**
67 * Convert a string if ASCII characters HASHVAL to its hexadecimal
68 * representation.
69 *
70 * The returned string will be allocated in the POOL and be null-terminated.
71 */
72static const char *
73hex_encode(const unsigned char *hashval,
74           apr_pool_t *pool)
75{
76    int i;
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);
81    }
82    hexval[APR_MD5_DIGESTSIZE * 2] = '\0';
83    return hexval;
84}
85
86/**
87 * Returns a 36-byte long string of random characters.
88 * UUIDs are formatted as: 00112233-4455-6677-8899-AABBCCDDEEFF.
89 *
90 * The returned string will be allocated in the POOL and be null-terminated.
91 */
92static const char *
93random_cnonce(apr_pool_t *pool)
94{
95    apr_uuid_t uuid;
96    char *buf = apr_palloc(pool, APR_UUID_FORMATTED_LENGTH + 1);
97
98    apr_uuid_get(&uuid);
99    apr_uuid_format(buf, &uuid);
100
101    return hex_encode((unsigned char*)buf, pool);
102}
103
104static apr_status_t
105build_digest_ha1(const char **out_ha1,
106                 const char *username,
107                 const char *password,
108                 const char *realm_name,
109                 apr_pool_t *pool)
110{
111    const char *tmp;
112    unsigned char ha1[APR_MD5_DIGESTSIZE];
113    apr_status_t status;
114
115    /* calculate ha1:
116       MD5 hash of the combined user name, authentication realm and password */
117    tmp = apr_psprintf(pool, "%s:%s:%s",
118                       username,
119                       realm_name,
120                       password);
121    status = apr_md5(ha1, tmp, strlen(tmp));
122    if (status)
123        return status;
124
125    *out_ha1 = hex_encode(ha1, pool);
126
127    return APR_SUCCESS;
128}
129
130static apr_status_t
131build_digest_ha2(const char **out_ha2,
132                 const char *uri,
133                 const char *method,
134                 const char *qop,
135                 apr_pool_t *pool)
136{
137    if (!qop || strcmp(qop, "auth") == 0) {
138        const char *tmp;
139        unsigned char ha2[APR_MD5_DIGESTSIZE];
140        apr_status_t status;
141
142        /* calculate ha2:
143           MD5 hash of the combined method and URI */
144        tmp = apr_psprintf(pool, "%s:%s",
145                           method,
146                           uri);
147        status = apr_md5(ha2, tmp, strlen(tmp));
148        if (status)
149            return status;
150
151        *out_ha2 = hex_encode(ha2, pool);
152
153        return APR_SUCCESS;
154    } else {
155        /* TODO: auth-int isn't supported! */
156        return APR_ENOTIMPL;
157    }
158}
159
160static apr_status_t
161build_auth_header(const char **out_header,
162                  digest_authn_info_t *digest_info,
163                  const char *path,
164                  const char *method,
165                  apr_pool_t *pool)
166{
167    char *hdr;
168    const char *ha2;
169    const char *response;
170    unsigned char response_hdr[APR_MD5_DIGESTSIZE];
171    const char *response_hdr_hex;
172    apr_status_t status;
173
174    status = build_digest_ha2(&ha2, path, method, digest_info->qop, pool);
175    if (status)
176        return status;
177
178    hdr = apr_psprintf(pool,
179                       "Digest realm=\"%s\","
180                       " username=\"%s\","
181                       " nonce=\"%s\","
182                       " uri=\"%s\"",
183                       digest_info->realm, digest_info->username,
184                       digest_info->nonce,
185                       path);
186
187    if (digest_info->qop) {
188        if (! digest_info->cnonce)
189            digest_info->cnonce = random_cnonce(digest_info->pool);
190
191        hdr = apr_psprintf(pool, "%s, nc=%08x, cnonce=\"%s\", qop=\"%s\"",
192                           hdr,
193                           digest_info->digest_nc,
194                           digest_info->cnonce,
195                           digest_info->qop);
196
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);
205    } else {
206        /* Build the response header:
207           MD5 hash of the combined HA1 result, server nonce (nonce)
208           and HA2 result. */
209        response = apr_psprintf(pool, "%s:%s:%s",
210                                digest_info->ha1, digest_info->nonce, ha2);
211    }
212
213    status = apr_md5(response_hdr, response, strlen(response));
214    if (status)
215        return status;
216
217    response_hdr_hex = hex_encode(response_hdr, pool);
218
219    hdr = apr_psprintf(pool, "%s, response=\"%s\"", hdr, response_hdr_hex);
220
221    if (digest_info->opaque) {
222        hdr = apr_psprintf(pool, "%s, opaque=\"%s\"", hdr,
223                           digest_info->opaque);
224    }
225    if (digest_info->algorithm) {
226        hdr = apr_psprintf(pool, "%s, algorithm=\"%s\"", hdr,
227                           digest_info->algorithm);
228    }
229
230    *out_header = hdr;
231
232    return APR_SUCCESS;
233}
234
235apr_status_t
236serf__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,
241                         void *baton,
242                         apr_pool_t *pool)
243{
244    char *attrs;
245    char *nextkv;
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;
251    const char *key;
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;
256    apr_status_t status;
257    apr_pool_t *cred_pool;
258    char *username, *password;
259
260    /* Can't do Digest authentication if there's no callback to get
261       username & password. */
262    if (!ctx->cred_cb) {
263        return SERF_ERROR_AUTHN_FAILED;
264    }
265
266    if (code == 401) {
267        authn_info = serf__get_authn_info_for_server(conn);
268    } else {
269        authn_info = &ctx->proxy_authn_info;
270    }
271    digest_info = authn_info->baton;
272
273    /* Need a copy cuz we're going to write NUL characters into the string.  */
274    attrs = apr_pstrdup(pool, auth_attr);
275
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) {
281        char *val;
282
283        val = strchr(key, '=');
284        if (val == NULL)
285            continue;
286        *val++ = '\0';
287
288        /* skip leading spaces */
289        while (*key && *key == ' ')
290            key++;
291
292        /* If the value is quoted, then remove the quotes.  */
293        if (*val == '"') {
294            apr_size_t last = strlen(val) - 1;
295
296            if (val[last] == '"') {
297                val[last] = '\0';
298                val++;
299            }
300        }
301
302        if (strcmp(key, "realm") == 0)
303            realm_name = val;
304        else if (strcmp(key, "nonce") == 0)
305            nonce = val;
306        else if (strcmp(key, "algorithm") == 0)
307            algorithm = val;
308        else if (strcmp(key, "qop") == 0)
309            qop = val;
310        else if (strcmp(key, "opaque") == 0)
311            opaque = val;
312
313        /* Ignore all unsupported attributes. */
314    }
315
316    if (!realm_name) {
317        return SERF_ERROR_AUTHN_MISSING_ATTRIBUTE;
318    }
319
320    realm = serf__construct_realm(code == 401 ? HOST : PROXY,
321                                  conn, realm_name,
322                                  pool);
323
324    /* Ask the application for credentials */
325    apr_pool_create(&cred_pool, pool);
326    status = serf__provide_credentials(ctx,
327                                       &username, &password,
328                                       request, baton,
329                                       code, authn_info->scheme->name,
330                                       realm, cred_pool);
331    if (status) {
332        apr_pool_destroy(cred_pool);
333        return status;
334    }
335
336    digest_info->header = (code == 401) ? "Authorization" :
337                                          "Proxy-Authorization";
338
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++;
354
355    status = build_digest_ha1(&digest_info->ha1, username, password,
356                              digest_info->realm, digest_info->pool);
357
358    apr_pool_destroy(cred_pool);
359
360    /* If the handshake is finished tell serf it can send as much requests as it
361       likes. */
362    serf_connection_set_max_outstanding_requests(conn, 0);
363
364    return status;
365}
366
367apr_status_t
368serf__init_digest(int code,
369                  serf_context_t *ctx,
370                  apr_pool_t *pool)
371{
372    return APR_SUCCESS;
373}
374
375apr_status_t
376serf__init_digest_connection(const serf__authn_scheme_t *scheme,
377                             int code,
378                             serf_connection_t *conn,
379                             apr_pool_t *pool)
380{
381    serf_context_t *ctx = conn->ctx;
382    serf__authn_info_t *authn_info;
383
384    if (code == 401) {
385        authn_info = serf__get_authn_info_for_server(conn);
386    } else {
387        authn_info = &ctx->proxy_authn_info;
388    }
389
390    if (!authn_info->baton) {
391        authn_info->baton = apr_pcalloc(pool, sizeof(digest_authn_info_t));
392    }
393
394    /* Make serf send the initial requests one by one */
395    serf_connection_set_max_outstanding_requests(conn, 1);
396
397    return APR_SUCCESS;
398}
399
400apr_status_t
401serf__setup_request_digest_auth(peer_t peer,
402                                int code,
403                                serf_connection_t *conn,
404                                serf_request_t *request,
405                                const char *method,
406                                const char *uri,
407                                serf_bucket_t *hdrs_bkt)
408{
409    serf_context_t *ctx = conn->ctx;
410    serf__authn_info_t *authn_info;
411    digest_authn_info_t *digest_info;
412    apr_status_t status;
413
414    if (peer == HOST) {
415        authn_info = serf__get_authn_info_for_server(conn);
416    } else {
417        authn_info = &ctx->proxy_authn_info;
418    }
419    digest_info = authn_info->baton;
420
421    if (digest_info && digest_info->realm) {
422        const char *value;
423        const char *path;
424
425        /* TODO: per request pool? */
426
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
429           case. */
430        if (strcmp(method, "CONNECT") == 0)
431            path = uri;
432        else {
433            apr_uri_t parsed_uri;
434
435            /* Extract path from uri. */
436            status = apr_uri_parse(conn->pool, uri, &parsed_uri);
437            if (status)
438                return status;
439
440            path = parsed_uri.path;
441        }
442
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,
447                                   conn->pool);
448        if (status)
449            return status;
450
451        serf_bucket_headers_setn(hdrs_bkt, digest_info->header,
452                                 value);
453        digest_info->digest_nc++;
454
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;
459    }
460
461    return APR_SUCCESS;
462}
463
464apr_status_t
465serf__validate_response_digest_auth(const serf__authn_scheme_t *scheme,
466                                    peer_t peer,
467                                    int code,
468                                    serf_connection_t *conn,
469                                    serf_request_t *request,
470                                    serf_bucket_t *response,
471                                    apr_pool_t *pool)
472{
473    const char *key;
474    char *auth_attr;
475    char *nextkv;
476    const char *rspauth = NULL;
477    const char *qop = NULL;
478    const char *nc_str = NULL;
479    serf_bucket_t *hdrs;
480    serf_context_t *ctx = conn->ctx;
481    apr_status_t status;
482
483    hdrs = serf_bucket_response_get_headers(response);
484
485    /* Need a copy cuz we're going to write NUL characters into the string.  */
486    if (peer == HOST)
487        auth_attr = apr_pstrdup(pool,
488            serf_bucket_headers_get(hdrs, "Authentication-Info"));
489    else
490        auth_attr = apr_pstrdup(pool,
491            serf_bucket_headers_get(hdrs, "Proxy-Authentication-Info"));
492
493    /* If there's no Authentication-Info header there's nothing to validate. */
494    if (! auth_attr)
495        return APR_SUCCESS;
496
497    /* We're expecting a list of key=value pairs, separated by a comma.
498       Ex. rspauth="8a4b8451084b082be6b105e2b7975087",
499       cnonce="346531653132652d303033392d3435", nc=00000007,
500       qop=auth */
501    for ( ; (key = apr_strtok(auth_attr, ",", &nextkv)) != NULL; auth_attr = NULL) {
502        char *val;
503
504        val = strchr(key, '=');
505        if (val == NULL)
506            continue;
507        *val++ = '\0';
508
509        /* skip leading spaces */
510        while (*key && *key == ' ')
511            key++;
512
513        /* If the value is quoted, then remove the quotes.  */
514        if (*val == '"') {
515            apr_size_t last = strlen(val) - 1;
516
517            if (val[last] == '"') {
518                val[last] = '\0';
519                val++;
520            }
521        }
522
523        if (strcmp(key, "rspauth") == 0)
524            rspauth = val;
525        else if (strcmp(key, "qop") == 0)
526            qop = val;
527        else if (strcmp(key, "nc") == 0)
528            nc_str = val;
529    }
530
531    if (rspauth) {
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;
537
538        if (peer == HOST) {
539            authn_info = serf__get_authn_info_for_server(conn);
540        } else {
541            authn_info = &ctx->proxy_authn_info;
542        }
543        digest_info = authn_info->baton;
544
545        status = build_digest_ha2(&ha2, req_uri, "", qop, pool);
546        if (status)
547            return status;
548
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);
554
555        /* Incorrect response-digest in Authentication-Info header. */
556        if (strcmp(rspauth, resp_hdr_hex) != 0) {
557            return SERF_ERROR_AUTHN_FAILED;
558        }
559    }
560
561    return APR_SUCCESS;
562}
563