1193323Sed/* ====================================================================
2193323Sed *    Licensed to the Apache Software Foundation (ASF) under one
3193323Sed *    or more contributor license agreements.  See the NOTICE file
4193323Sed *    distributed with this work for additional information
5193323Sed *    regarding copyright ownership.  The ASF licenses this file
6193323Sed *    to you under the Apache License, Version 2.0 (the
7193323Sed *    "License"); you may not use this file except in compliance
8193323Sed *    with the License.  You may obtain a copy of the License at
9193323Sed *
10193323Sed *      http://www.apache.org/licenses/LICENSE-2.0
11193323Sed *
12193323Sed *    Unless required by applicable law or agreed to in writing,
13193323Sed *    software distributed under the License is distributed on an
14193323Sed *    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15193323Sed *    KIND, either express or implied.  See the License for the
16193323Sed *    specific language governing permissions and limitations
17193323Sed *    under the License.
18193323Sed * ====================================================================
19210006Srdivacky */
20218885Sdim
21193323Sed/*** Digest authentication ***/
22193323Sed
23193323Sed#include <serf.h>
24193323Sed#include <serf_private.h>
25193323Sed#include <auth/auth.h>
26193323Sed
27193323Sed#include <apr.h>
28193323Sed#include <apr_base64.h>
29193323Sed#include <apr_strings.h>
30193323Sed#include <apr_uuid.h>
31193323Sed#include <apr_md5.h>
32193323Sed
33193323Sed/** Digest authentication, implements RFC 2617. **/
34195340Sed
35193323Sed/* TODO: add support for the domain attribute. This defines the protection
36193323Sed   space, so that serf can decide per URI if it should reuse the cached
37193323Sed   credentials for the server, or not. */
38193323Sed
39193323Sed/* Stores the context information related to Digest authentication.
40193323Sed   This information is stored in the per server cache in the serf context. */
41193323Sedtypedef struct digest_authn_info_t {
42193323Sed    /* nonce-count for digest authentication */
43193323Sed    unsigned int digest_nc;
44193323Sed
45193323Sed    const char *header;
46193323Sed
47195340Sed    const char *ha1;
48198090Srdivacky
49193323Sed    const char *realm;
50193323Sed    const char *cnonce;
51212793Sdim    const char *nonce;
52193323Sed    const char *opaque;
53193323Sed    const char *algorithm;
54193323Sed    const char *qop;
55193323Sed    const char *username;
56193323Sed
57193323Sed    apr_pool_t *pool;
58205407Srdivacky} digest_authn_info_t;
59193323Sed
60193323Sedstatic char
61193323Sedint_to_hex(int v)
62193323Sed{
63193323Sed    return (v < 10) ? '0' + v : 'a' + (v - 10);
64193323Sed}
65212793Sdim
66205407Srdivacky/**
67205407Srdivacky * Convert a string if ASCII characters HASHVAL to its hexadecimal
68206083Srdivacky * representation.
69193323Sed *
70198090Srdivacky * The returned string will be allocated in the POOL and be null-terminated.
71193323Sed */
72212793Sdimstatic const char *
73195340Sedhex_encode(const unsigned char *hashval,
74193323Sed           apr_pool_t *pool)
75193323Sed{
76193323Sed    int i;
77193323Sed    char *hexval = apr_palloc(pool, (APR_MD5_DIGESTSIZE * 2) + 1);
78212793Sdim    for (i = 0; i < APR_MD5_DIGESTSIZE; i++) {
79212793Sdim        hexval[2 * i] = int_to_hex((hashval[i] >> 4) & 0xf);
80193323Sed        hexval[2 * i + 1] = int_to_hex(hashval[i] & 0xf);
81193323Sed    }
82212793Sdim    hexval[APR_MD5_DIGESTSIZE * 2] = '\0';
83193323Sed    return hexval;
84193323Sed}
85193323Sed
86193323Sed/**
87193323Sed * Returns a 36-byte long string of random characters.
88193323Sed * UUIDs are formatted as: 00112233-4455-6677-8899-AABBCCDDEEFF.
89193323Sed *
90207618Srdivacky * The returned string will be allocated in the POOL and be null-terminated.
91193323Sed */
92193323Sedstatic const char *
93193323Sedrandom_cnonce(apr_pool_t *pool)
94193323Sed{
95193323Sed    apr_uuid_t uuid;
96193323Sed    char *buf = apr_palloc(pool, APR_UUID_FORMATTED_LENGTH + 1);
97193323Sed
98193323Sed    apr_uuid_get(&uuid);
99193323Sed    apr_uuid_format(buf, &uuid);
100193323Sed
101207618Srdivacky    return hex_encode((unsigned char*)buf, pool);
102193323Sed}
103193323Sed
104193323Sedstatic apr_status_t
105193323Sedbuild_digest_ha1(const char **out_ha1,
106207618Srdivacky                 const char *username,
107193323Sed                 const char *password,
108193323Sed                 const char *realm_name,
109193323Sed                 apr_pool_t *pool)
110193323Sed{
111193323Sed    const char *tmp;
112193323Sed    unsigned char ha1[APR_MD5_DIGESTSIZE];
113193323Sed    apr_status_t status;
114193323Sed
115193323Sed    /* calculate ha1:
116193323Sed       MD5 hash of the combined user name, authentication realm and password */
117193323Sed    tmp = apr_psprintf(pool, "%s:%s:%s",
118193323Sed                       username,
119193323Sed                       realm_name,
120207618Srdivacky                       password);
121207618Srdivacky    status = apr_md5(ha1, tmp, strlen(tmp));
122193323Sed    if (status)
123193323Sed        return status;
124193323Sed
125193323Sed    *out_ha1 = hex_encode(ha1, pool);
126207618Srdivacky
127193323Sed    return APR_SUCCESS;
128193323Sed}
129193323Sed
130193323Sedstatic apr_status_t
131193323Sedbuild_digest_ha2(const char **out_ha2,
132193323Sed                 const char *uri,
133193323Sed                 const char *method,
134212793Sdim                 const char *qop,
135212793Sdim                 apr_pool_t *pool)
136193323Sed{
137193323Sed    if (!qop || strcmp(qop, "auth") == 0) {
138193323Sed        const char *tmp;
139193323Sed        unsigned char ha2[APR_MD5_DIGESTSIZE];
140193323Sed        apr_status_t status;
141193323Sed
142193323Sed        /* calculate ha2:
143193323Sed           MD5 hash of the combined method and URI */
144193323Sed        tmp = apr_psprintf(pool, "%s:%s",
145193323Sed                           method,
146193323Sed                           uri);
147193323Sed        status = apr_md5(ha2, tmp, strlen(tmp));
148193323Sed        if (status)
149193323Sed            return status;
150193323Sed
151193323Sed        *out_ha2 = hex_encode(ha2, pool);
152193323Sed
153193323Sed        return APR_SUCCESS;
154193323Sed    } else {
155193323Sed        /* TODO: auth-int isn't supported! */
156193323Sed        return APR_ENOTIMPL;
157193323Sed    }
158193323Sed}
159193323Sed
160193323Sedstatic apr_status_t
161193323Sedbuild_auth_header(const char **out_header,
162193323Sed                  digest_authn_info_t *digest_info,
163207618Srdivacky                  const char *path,
164207618Srdivacky                  const char *method,
165207618Srdivacky                  apr_pool_t *pool)
166193323Sed{
167212793Sdim    char *hdr;
168193323Sed    const char *ha2;
169193323Sed    const char *response;
170207618Srdivacky    unsigned char response_hdr[APR_MD5_DIGESTSIZE];
171193323Sed    const char *response_hdr_hex;
172212793Sdim    apr_status_t status;
173212793Sdim
174207618Srdivacky    status = build_digest_ha2(&ha2, path, method, digest_info->qop, pool);
175207618Srdivacky    if (status)
176207618Srdivacky        return status;
177212793Sdim
178193323Sed    hdr = apr_psprintf(pool,
179193323Sed                       "Digest realm=\"%s\","
180193323Sed                       " username=\"%s\","
181207618Srdivacky                       " nonce=\"%s\","
182207618Srdivacky                       " uri=\"%s\"",
183193323Sed                       digest_info->realm, digest_info->username,
184212793Sdim                       digest_info->nonce,
185212793Sdim                       path);
186212793Sdim
187193323Sed    if (digest_info->qop) {
188193323Sed        if (! digest_info->cnonce)
189193323Sed            digest_info->cnonce = random_cnonce(digest_info->pool);
190193323Sed
191193323Sed        hdr = apr_psprintf(pool, "%s, nc=%08x, cnonce=\"%s\", qop=\"%s\"",
192193323Sed                           hdr,
193193323Sed                           digest_info->digest_nc,
194193323Sed                           digest_info->cnonce,
195193323Sed                           digest_info->qop);
196193323Sed
197193323Sed        /* Build the response header:
198207618Srdivacky           MD5 hash of the combined HA1 result, server nonce (nonce),
199207618Srdivacky           request counter (nc), client nonce (cnonce),
200193323Sed           quality of protection code (qop) and HA2 result. */
201212793Sdim        response = apr_psprintf(pool, "%s:%s:%08x:%s:%s:%s",
202212793Sdim                                digest_info->ha1, digest_info->nonce,
203193323Sed                                digest_info->digest_nc,
204207618Srdivacky                                digest_info->cnonce, digest_info->qop, ha2);
205212793Sdim    } else {
206207618Srdivacky        /* Build the response header:
207212793Sdim           MD5 hash of the combined HA1 result, server nonce (nonce)
208212793Sdim           and HA2 result. */
209193323Sed        response = apr_psprintf(pool, "%s:%s:%s",
210212793Sdim                                digest_info->ha1, digest_info->nonce, ha2);
211212793Sdim    }
212193323Sed
213193323Sed    status = apr_md5(response_hdr, response, strlen(response));
214193323Sed    if (status)
215193323Sed        return status;
216193323Sed
217193323Sed    response_hdr_hex = hex_encode(response_hdr, pool);
218212793Sdim
219193323Sed    hdr = apr_psprintf(pool, "%s, response=\"%s\"", hdr, response_hdr_hex);
220193323Sed
221193323Sed    if (digest_info->opaque) {
222193323Sed        hdr = apr_psprintf(pool, "%s, opaque=\"%s\"", hdr,
223193323Sed                           digest_info->opaque);
224193323Sed    }
225193323Sed    if (digest_info->algorithm) {
226193323Sed        hdr = apr_psprintf(pool, "%s, algorithm=\"%s\"", hdr,
227193323Sed                           digest_info->algorithm);
228193323Sed    }
229193323Sed
230193323Sed    *out_header = hdr;
231193323Sed
232193323Sed    return APR_SUCCESS;
233193323Sed}
234193323Sed
235193323Sedapr_status_t
236193323Sedserf__handle_digest_auth(int code,
237193323Sed                         serf_request_t *request,
238193323Sed                         serf_bucket_t *response,
239193323Sed                         const char *auth_hdr,
240193323Sed                         const char *auth_attr,
241193323Sed                         void *baton,
242193323Sed                         apr_pool_t *pool)
243193323Sed{
244193323Sed    char *attrs;
245212793Sdim    char *nextkv;
246193323Sed    const char *realm, *realm_name = NULL;
247193323Sed    const char *nonce = NULL;
248193323Sed    const char *algorithm = NULL;
249193323Sed    const char *qop = NULL;
250193323Sed    const char *opaque = NULL;
251193323Sed    const char *key;
252193323Sed    serf_connection_t *conn = request->conn;
253193323Sed    serf_context_t *ctx = conn->ctx;
254198090Srdivacky    serf__authn_info_t *authn_info;
255193323Sed    digest_authn_info_t *digest_info;
256193323Sed    apr_status_t status;
257193323Sed    apr_pool_t *cred_pool;
258212793Sdim    char *username, *password;
259212793Sdim
260193323Sed    /* Can't do Digest authentication if there's no callback to get
261193323Sed       username & password. */
262193323Sed    if (!ctx->cred_cb) {
263193323Sed        return SERF_ERROR_AUTHN_FAILED;
264193323Sed    }
265193323Sed
266193323Sed    if (code == 401) {
267193323Sed        authn_info = serf__get_authn_info_for_server(conn);
268193323Sed    } else {
269193323Sed        authn_info = &ctx->proxy_authn_info;
270193323Sed    }
271212793Sdim    digest_info = authn_info->baton;
272210006Srdivacky
273193323Sed    /* Need a copy cuz we're going to write NUL characters into the string.  */
274193323Sed    attrs = apr_pstrdup(pool, auth_attr);
275193323Sed
276193323Sed    /* We're expecting a list of key=value pairs, separated by a comma.
277212793Sdim       Ex. realm="SVN Digest",
278193323Sed       nonce="f+zTl/leBAA=e371bd3070adfb47b21f5fc64ad8cc21adc371a5",
279193323Sed       algorithm=MD5, qop="auth" */
280193323Sed    for ( ; (key = apr_strtok(attrs, ",", &nextkv)) != NULL; attrs = NULL) {
281193323Sed        char *val;
282193323Sed
283193323Sed        val = strchr(key, '=');
284212793Sdim        if (val == NULL)
285212793Sdim            continue;
286193323Sed        *val++ = '\0';
287193323Sed
288212793Sdim        /* skip leading spaces */
289193323Sed        while (*key && *key == ' ')
290193323Sed            key++;
291193323Sed
292193323Sed        /* If the value is quoted, then remove the quotes.  */
293193323Sed        if (*val == '"') {
294193323Sed            apr_size_t last = strlen(val) - 1;
295193323Sed
296193323Sed            if (val[last] == '"') {
297193323Sed                val[last] = '\0';
298193323Sed                val++;
299193323Sed            }
300195340Sed        }
301195340Sed
302193323Sed        if (strcmp(key, "realm") == 0)
303193323Sed            realm_name = val;
304193323Sed        else if (strcmp(key, "nonce") == 0)
305193323Sed            nonce = val;
306193323Sed        else if (strcmp(key, "algorithm") == 0)
307212793Sdim            algorithm = val;
308193323Sed        else if (strcmp(key, "qop") == 0)
309193323Sed            qop = val;
310193323Sed        else if (strcmp(key, "opaque") == 0)
311193323Sed            opaque = val;
312193323Sed
313193323Sed        /* Ignore all unsupported attributes. */
314193323Sed    }
315193323Sed
316193323Sed    if (!realm_name) {
317193323Sed        return SERF_ERROR_AUTHN_MISSING_ATTRIBUTE;
318193323Sed    }
319193323Sed
320193323Sed    realm = serf__construct_realm(code == 401 ? HOST : PROXY,
321193323Sed                                  conn, realm_name,
322193323Sed                                  pool);
323193323Sed
324193323Sed    /* Ask the application for credentials */
325193323Sed    apr_pool_create(&cred_pool, pool);
326218885Sdim    status = serf__provide_credentials(ctx,
327193323Sed                                       &username, &password,
328193323Sed                                       request, baton,
329193323Sed                                       code, authn_info->scheme->name,
330193323Sed                                       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