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 apr_status_t
100build_digest_ha1(const char **out_ha1,
101                 const char *username,
102                 const char *password,
103                 const char *realm_name,
104                 apr_pool_t *pool)
105{
106    const char *tmp;
107    unsigned char ha1[APR_MD5_DIGESTSIZE];
108    apr_status_t status;
109
110    /* calculate ha1:
111       MD5 hash of the combined user name, authentication realm and password */
112    tmp = apr_psprintf(pool, "%s:%s:%s",
113                       username,
114                       realm_name,
115                       password);
116    status = apr_md5(ha1, tmp, strlen(tmp));
117    if (status)
118        return status;
119
120    *out_ha1 = hex_encode(ha1, pool);
121
122    return APR_SUCCESS;
123}
124
125static apr_status_t
126build_digest_ha2(const char **out_ha2,
127                 const char *uri,
128                 const char *method,
129                 const char *qop,
130                 apr_pool_t *pool)
131{
132    if (!qop || strcmp(qop, "auth") == 0) {
133        const char *tmp;
134        unsigned char ha2[APR_MD5_DIGESTSIZE];
135        apr_status_t status;
136
137        /* calculate ha2:
138           MD5 hash of the combined method and URI */
139        tmp = apr_psprintf(pool, "%s:%s",
140                           method,
141                           uri);
142        status = apr_md5(ha2, tmp, strlen(tmp));
143        if (status)
144            return status;
145
146        *out_ha2 = hex_encode(ha2, pool);
147
148        return APR_SUCCESS;
149    } else {
150        /* TODO: auth-int isn't supported! */
151        return APR_ENOTIMPL;
152    }
153}
154
155static apr_status_t
156build_auth_header(const char **out_header,
157                  digest_authn_info_t *digest_info,
158                  const char *path,
159                  const char *method,
160                  apr_pool_t *pool)
161{
162    char *hdr;
163    const char *ha2;
164    const char *response;
165    unsigned char response_hdr[APR_MD5_DIGESTSIZE];
166    const char *response_hdr_hex;
167    apr_status_t status;
168
169    status = build_digest_ha2(&ha2, path, method, digest_info->qop, pool);
170    if (status)
171        return status;
172
173    hdr = apr_psprintf(pool,
174                       "Digest realm=\"%s\","
175                       " username=\"%s\","
176                       " nonce=\"%s\","
177                       " uri=\"%s\"",
178                       digest_info->realm, digest_info->username,
179                       digest_info->nonce,
180                       path);
181
182    if (digest_info->qop) {
183        if (! digest_info->cnonce)
184            digest_info->cnonce = random_cnonce(digest_info->pool);
185
186        hdr = apr_psprintf(pool, "%s, nc=%08x, cnonce=\"%s\", qop=\"%s\"",
187                           hdr,
188                           digest_info->digest_nc,
189                           digest_info->cnonce,
190                           digest_info->qop);
191
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);
200    } else {
201        /* Build the response header:
202           MD5 hash of the combined HA1 result, server nonce (nonce)
203           and HA2 result. */
204        response = apr_psprintf(pool, "%s:%s:%s",
205                                digest_info->ha1, digest_info->nonce, ha2);
206    }
207
208    status = apr_md5(response_hdr, response, strlen(response));
209    if (status)
210        return status;
211
212    response_hdr_hex = hex_encode(response_hdr, pool);
213
214    hdr = apr_psprintf(pool, "%s, response=\"%s\"", hdr, response_hdr_hex);
215
216    if (digest_info->opaque) {
217        hdr = apr_psprintf(pool, "%s, opaque=\"%s\"", hdr,
218                           digest_info->opaque);
219    }
220    if (digest_info->algorithm) {
221        hdr = apr_psprintf(pool, "%s, algorithm=\"%s\"", hdr,
222                           digest_info->algorithm);
223    }
224
225    *out_header = hdr;
226
227    return APR_SUCCESS;
228}
229
230apr_status_t
231serf__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,
236                         void *baton,
237                         apr_pool_t *pool)
238{
239    char *attrs;
240    char *nextkv;
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;
246    const char *key;
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;
251    apr_status_t status;
252    apr_pool_t *cred_pool;
253    char *username, *password;
254
255    /* Can't do Digest authentication if there's no callback to get
256       username & password. */
257    if (!ctx->cred_cb) {
258        return SERF_ERROR_AUTHN_FAILED;
259    }
260
261    if (code == 401) {
262        authn_info = serf__get_authn_info_for_server(conn);
263    } else {
264        authn_info = &ctx->proxy_authn_info;
265    }
266    digest_info = authn_info->baton;
267
268    /* Need a copy cuz we're going to write NUL characters into the string.  */
269    attrs = apr_pstrdup(pool, auth_attr);
270
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) {
276        char *val;
277
278        val = strchr(key, '=');
279        if (val == NULL)
280            continue;
281        *val++ = '\0';
282
283        /* skip leading spaces */
284        while (*key && *key == ' ')
285            key++;
286
287        /* If the value is quoted, then remove the quotes.  */
288        if (*val == '"') {
289            apr_size_t last = strlen(val) - 1;
290
291            if (val[last] == '"') {
292                val[last] = '\0';
293                val++;
294            }
295        }
296
297        if (strcmp(key, "realm") == 0)
298            realm_name = val;
299        else if (strcmp(key, "nonce") == 0)
300            nonce = val;
301        else if (strcmp(key, "algorithm") == 0)
302            algorithm = val;
303        else if (strcmp(key, "qop") == 0)
304            qop = val;
305        else if (strcmp(key, "opaque") == 0)
306            opaque = val;
307
308        /* Ignore all unsupported attributes. */
309    }
310
311    if (!realm_name) {
312        return SERF_ERROR_AUTHN_MISSING_ATTRIBUTE;
313    }
314
315    realm = serf__construct_realm(code == 401 ? HOST : PROXY,
316                                  conn, realm_name,
317                                  pool);
318
319    /* Ask the application for credentials */
320    apr_pool_create(&cred_pool, pool);
321    status = serf__provide_credentials(ctx,
322                                       &username, &password,
323                                       request, baton,
324                                       code, authn_info->scheme->name,
325                                       realm, cred_pool);
326    if (status) {
327        apr_pool_destroy(cred_pool);
328        return status;
329    }
330
331    digest_info->header = (code == 401) ? "Authorization" :
332                                          "Proxy-Authorization";
333
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++;
349
350    status = build_digest_ha1(&digest_info->ha1, username, password,
351                              digest_info->realm, digest_info->pool);
352
353    apr_pool_destroy(cred_pool);
354
355    /* If the handshake is finished tell serf it can send as much requests as it
356       likes. */
357    serf_connection_set_max_outstanding_requests(conn, 0);
358
359    return status;
360}
361
362apr_status_t
363serf__init_digest(int code,
364                  serf_context_t *ctx,
365                  apr_pool_t *pool)
366{
367    return APR_SUCCESS;
368}
369
370apr_status_t
371serf__init_digest_connection(const serf__authn_scheme_t *scheme,
372                             int code,
373                             serf_connection_t *conn,
374                             apr_pool_t *pool)
375{
376    serf_context_t *ctx = conn->ctx;
377    serf__authn_info_t *authn_info;
378
379    if (code == 401) {
380        authn_info = serf__get_authn_info_for_server(conn);
381    } else {
382        authn_info = &ctx->proxy_authn_info;
383    }
384
385    if (!authn_info->baton) {
386        authn_info->baton = apr_pcalloc(pool, sizeof(digest_authn_info_t));
387    }
388
389    /* Make serf send the initial requests one by one */
390    serf_connection_set_max_outstanding_requests(conn, 1);
391
392    return APR_SUCCESS;
393}
394
395apr_status_t
396serf__setup_request_digest_auth(peer_t peer,
397                                int code,
398                                serf_connection_t *conn,
399                                serf_request_t *request,
400                                const char *method,
401                                const char *uri,
402                                serf_bucket_t *hdrs_bkt)
403{
404    serf_context_t *ctx = conn->ctx;
405    serf__authn_info_t *authn_info;
406    digest_authn_info_t *digest_info;
407    apr_status_t status;
408
409    if (peer == HOST) {
410        authn_info = serf__get_authn_info_for_server(conn);
411    } else {
412        authn_info = &ctx->proxy_authn_info;
413    }
414    digest_info = authn_info->baton;
415
416    if (digest_info && digest_info->realm) {
417        const char *value;
418        const char *path;
419
420        /* TODO: per request pool? */
421
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
424           case. */
425        if (strcmp(method, "CONNECT") == 0)
426            path = uri;
427        else {
428            apr_uri_t parsed_uri;
429
430            /* Extract path from uri. */
431            status = apr_uri_parse(conn->pool, uri, &parsed_uri);
432            if (status)
433                return status;
434
435            path = parsed_uri.path;
436        }
437
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,
442                                   conn->pool);
443        if (status)
444            return status;
445
446        serf_bucket_headers_setn(hdrs_bkt, digest_info->header,
447                                 value);
448        digest_info->digest_nc++;
449
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;
454    }
455
456    return APR_SUCCESS;
457}
458
459apr_status_t
460serf__validate_response_digest_auth(const serf__authn_scheme_t *scheme,
461                                    peer_t peer,
462                                    int code,
463                                    serf_connection_t *conn,
464                                    serf_request_t *request,
465                                    serf_bucket_t *response,
466                                    apr_pool_t *pool)
467{
468    const char *key;
469    char *auth_attr;
470    char *nextkv;
471    const char *rspauth = NULL;
472    const char *qop = NULL;
473    const char *nc_str = NULL;
474    serf_bucket_t *hdrs;
475    serf_context_t *ctx = conn->ctx;
476    apr_status_t status;
477
478    hdrs = serf_bucket_response_get_headers(response);
479
480    /* Need a copy cuz we're going to write NUL characters into the string.  */
481    if (peer == HOST)
482        auth_attr = apr_pstrdup(pool,
483            serf_bucket_headers_get(hdrs, "Authentication-Info"));
484    else
485        auth_attr = apr_pstrdup(pool,
486            serf_bucket_headers_get(hdrs, "Proxy-Authentication-Info"));
487
488    /* If there's no Authentication-Info header there's nothing to validate. */
489    if (! auth_attr)
490        return APR_SUCCESS;
491
492    /* We're expecting a list of key=value pairs, separated by a comma.
493       Ex. rspauth="8a4b8451084b082be6b105e2b7975087",
494       cnonce="346531653132652d303033392d3435", nc=00000007,
495       qop=auth */
496    for ( ; (key = apr_strtok(auth_attr, ",", &nextkv)) != NULL; auth_attr = NULL) {
497        char *val;
498
499        val = strchr(key, '=');
500        if (val == NULL)
501            continue;
502        *val++ = '\0';
503
504        /* skip leading spaces */
505        while (*key && *key == ' ')
506            key++;
507
508        /* If the value is quoted, then remove the quotes.  */
509        if (*val == '"') {
510            apr_size_t last = strlen(val) - 1;
511
512            if (val[last] == '"') {
513                val[last] = '\0';
514                val++;
515            }
516        }
517
518        if (strcmp(key, "rspauth") == 0)
519            rspauth = val;
520        else if (strcmp(key, "qop") == 0)
521            qop = val;
522        else if (strcmp(key, "nc") == 0)
523            nc_str = val;
524    }
525
526    if (rspauth) {
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;
532
533        if (peer == HOST) {
534            authn_info = serf__get_authn_info_for_server(conn);
535        } else {
536            authn_info = &ctx->proxy_authn_info;
537        }
538        digest_info = authn_info->baton;
539
540        status = build_digest_ha2(&ha2, req_uri, "", qop, pool);
541        if (status)
542            return status;
543
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);
549
550        /* Incorrect response-digest in Authentication-Info header. */
551        if (strcmp(rspauth, resp_hdr_hex) != 0) {
552            return SERF_ERROR_AUTHN_FAILED;
553        }
554    }
555
556    return APR_SUCCESS;
557}
558