]> CyberLeo.Net >> Repos - FreeBSD/releng/10.0.git/blob - contrib/serf/auth/auth_digest.c
- Copy stable/10 (r259064) to releng/10.0 as part of the
[FreeBSD/releng/10.0.git] / contrib / serf / auth / auth_digest.c
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. */
36 typedef 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
55 static char
56 int_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  */
67 static const char *
68 hex_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  */
87 static const char *
88 random_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
99 static const char *
100 build_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
120 static const char *
121 build_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
146 static const char *
147 build_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
213 apr_status_t
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,
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
345 apr_status_t
346 serf__init_digest(int code,
347                   serf_context_t *ctx,
348                   apr_pool_t *pool)
349 {
350     return APR_SUCCESS;
351 }
352
353 apr_status_t
354 serf__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
378 apr_status_t
379 serf__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
440 apr_status_t
441 serf__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 }