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