[mod_openssl] remove erroneous SSL_set_shutdown()
[lighttpd.git] / src / mod_secdownload.c
blobce233cba08f719698d74e4ef04e7eed0344c7faf
1 #include "first.h"
3 #include "base.h"
4 #include "log.h"
5 #include "buffer.h"
6 #include "base64.h"
8 #include "plugin.h"
10 #include <stdlib.h>
11 #include <string.h>
13 #if defined HAVE_LIBSSL && defined HAVE_OPENSSL_SSL_H
14 #define USE_OPENSSL_CRYPTO
15 #endif
17 #ifdef USE_OPENSSL_CRYPTO
18 #include <openssl/evp.h>
19 #include <openssl/hmac.h>
20 #endif
22 #include "md5.h"
24 #define HASHLEN 16
25 typedef unsigned char HASH[HASHLEN];
26 #define HASHHEXLEN 32
27 typedef char HASHHEX[HASHHEXLEN+1];
30 * mod_secdownload verifies a checksum associated with a timestamp
31 * and a path.
33 * It takes an URL of the form:
34 * securl := <uri-prefix> <mac> <protected-path>
35 * uri-prefix := '/' any* # whatever was configured: must start with a '/')
36 * mac := [a-zA-Z0-9_-]{mac_len} # mac length depends on selected algorithm
37 * protected-path := '/' <timestamp> <rel-path>
38 * timestamp := [a-f0-9]{8} # timestamp when the checksum was calculated
39 * # to prevent access after timeout (active requests
40 * # will finish successfully even after the timeout)
41 * rel-path := '/' any* # the protected path; changing the path breaks the
42 * # checksum
44 * The timestamp is the `epoch` timestamp in hex, i.e. time in seconds
45 * since 00:00:00 UTC on 1 January 1970.
47 * mod_secdownload supports various MAC algorithms:
49 * # md5
50 * mac_len := 32 (and hex only)
51 * mac := md5-hex(<secrect><rel-path><timestamp>) # lowercase hex
52 * perl example:
53 use Digest::MD5 qw(md5_hex);
54 my $secret = "verysecret";
55 my $rel_path = "/index.html"
56 my $xtime = sprintf("%08x", time);
57 my $url = '/'. md5_hex($secret . $rel_path . $xtime) . '/' . $xtime . $rel_path;
59 * # hmac-sha1
60 * mac_len := 27 (no base64 padding)
61 * mac := base64-url(hmac-sha1(<secret>, <protected-path>))
62 * perl example:
63 use Digest::SHA qw(hmac_sha1);
64 use MIME::Base64 qw(encode_base64url);
65 my $secret = "verysecret";
66 my $rel_path = "/index.html"
67 my $protected_path = '/' . sprintf("%08x", time) . $rel_path;
68 my $url = '/'. encode_base64url(hmac_sha1($protected_path, $secret)) . $protected_path;
70 * # hmac-256
71 * mac_len := 43 (no base64 padding)
72 * mac := base64-url(hmac-256(<secret>, <protected-path>))
73 use Digest::SHA qw(hmac_sha256);
74 use MIME::Base64 qw(encode_base64url);
75 my $secret = "verysecret";
76 my $rel_path = "/index.html"
77 my $protected_path = '/' . sprintf("%08x", time) . $rel_path;
78 my $url = '/'. encode_base64url(hmac_sha256($protected_path, $secret)) . $protected_path;
82 /* plugin config for all request/connections */
84 typedef enum {
85 SECDL_INVALID = 0,
86 SECDL_MD5 = 1,
87 SECDL_HMAC_SHA1 = 2,
88 SECDL_HMAC_SHA256 = 3,
89 } secdl_algorithm;
91 typedef struct {
92 buffer *doc_root;
93 buffer *secret;
94 buffer *uri_prefix;
95 secdl_algorithm algorithm;
97 unsigned int timeout;
98 unsigned short path_segments;
99 unsigned short hash_querystr;
100 } plugin_config;
102 typedef struct {
103 PLUGIN_DATA;
105 plugin_config **config_storage;
107 plugin_config conf;
108 } plugin_data;
110 static int const_time_memeq(const char *a, const char *b, size_t len) {
111 /* constant time memory compare, unless the compiler figures it out */
112 char diff = 0;
113 size_t i;
114 for (i = 0; i < len; ++i) {
115 diff |= (a[i] ^ b[i]);
117 return 0 == diff;
120 static const char* secdl_algorithm_names[] = {
121 "invalid",
122 "md5",
123 "hmac-sha1",
124 "hmac-sha256",
127 static secdl_algorithm algorithm_from_string(buffer *name) {
128 size_t ndx;
130 if (buffer_string_is_empty(name)) return SECDL_INVALID;
132 for (ndx = 1; ndx < sizeof(secdl_algorithm_names)/sizeof(secdl_algorithm_names[0]); ++ndx) {
133 if (0 == strcmp(secdl_algorithm_names[ndx], name->ptr)) return (secdl_algorithm)ndx;
136 return SECDL_INVALID;
139 static size_t secdl_algorithm_mac_length(secdl_algorithm alg) {
140 switch (alg) {
141 case SECDL_INVALID:
142 break;
143 case SECDL_MD5:
144 return 32;
145 case SECDL_HMAC_SHA1:
146 return 27;
147 case SECDL_HMAC_SHA256:
148 return 43;
150 return 0;
153 static int secdl_verify_mac(server *srv, plugin_config *config, const char* protected_path, const char* mac, size_t maclen) {
154 UNUSED(srv);
155 if (0 == maclen || secdl_algorithm_mac_length(config->algorithm) != maclen) return 0;
157 switch (config->algorithm) {
158 case SECDL_INVALID:
159 break;
160 case SECDL_MD5:
162 li_MD5_CTX Md5Ctx;
163 HASH HA1;
164 char hexmd5[33];
165 const char *ts_str;
166 const char *rel_uri;
168 /* legacy message:
169 * protected_path := '/' <timestamp-hex> <rel-path>
170 * timestamp-hex := [0-9a-f]{8}
171 * rel-path := '/' any*
172 * (the protected path was already verified)
173 * message = <secret><rel-path><timestamp-hex>
175 ts_str = protected_path + 1;
176 rel_uri = ts_str + 8;
178 li_MD5_Init(&Md5Ctx);
179 li_MD5_Update(&Md5Ctx, CONST_BUF_LEN(config->secret));
180 li_MD5_Update(&Md5Ctx, rel_uri, strlen(rel_uri));
181 li_MD5_Update(&Md5Ctx, ts_str, 8);
182 li_MD5_Final(HA1, &Md5Ctx);
184 li_tohex(hexmd5, sizeof(hexmd5), (const char *)HA1, 16);
186 return (32 == maclen) && const_time_memeq(mac, hexmd5, 32);
188 case SECDL_HMAC_SHA1:
189 #ifdef USE_OPENSSL_CRYPTO
191 unsigned char digest[20];
192 char base64_digest[27];
194 if (NULL == HMAC(
195 EVP_sha1(),
196 (unsigned char const*) CONST_BUF_LEN(config->secret),
197 (unsigned char const*) protected_path, strlen(protected_path),
198 digest, NULL)) {
199 log_error_write(srv, __FILE__, __LINE__, "s",
200 "hmac-sha1: HMAC() failed");
201 return 0;
204 li_to_base64_no_padding(base64_digest, 27, digest, 20, BASE64_URL);
206 return (27 == maclen) && const_time_memeq(mac, base64_digest, 27);
208 #endif
209 break;
210 case SECDL_HMAC_SHA256:
211 #ifdef USE_OPENSSL_CRYPTO
213 unsigned char digest[32];
214 char base64_digest[43];
216 if (NULL == HMAC(
217 EVP_sha256(),
218 (unsigned char const*) CONST_BUF_LEN(config->secret),
219 (unsigned char const*) protected_path, strlen(protected_path),
220 digest, NULL)) {
221 log_error_write(srv, __FILE__, __LINE__, "s",
222 "hmac-sha256: HMAC() failed");
223 return 0;
226 li_to_base64_no_padding(base64_digest, 43, digest, 32, BASE64_URL);
228 return (43 == maclen) && const_time_memeq(mac, base64_digest, 43);
230 #endif
231 break;
234 return 0;
237 /* init the plugin data */
238 INIT_FUNC(mod_secdownload_init) {
239 plugin_data *p;
241 p = calloc(1, sizeof(*p));
243 return p;
246 /* detroy the plugin data */
247 FREE_FUNC(mod_secdownload_free) {
248 plugin_data *p = p_d;
249 UNUSED(srv);
251 if (!p) return HANDLER_GO_ON;
253 if (p->config_storage) {
254 size_t i;
255 for (i = 0; i < srv->config_context->used; i++) {
256 plugin_config *s = p->config_storage[i];
258 if (NULL == s) continue;
260 buffer_free(s->secret);
261 buffer_free(s->doc_root);
262 buffer_free(s->uri_prefix);
264 free(s);
266 free(p->config_storage);
269 free(p);
271 return HANDLER_GO_ON;
274 /* handle plugin config and check values */
276 SETDEFAULTS_FUNC(mod_secdownload_set_defaults) {
277 plugin_data *p = p_d;
278 size_t i = 0;
280 config_values_t cv[] = {
281 { "secdownload.secret", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION }, /* 0 */
282 { "secdownload.document-root", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION }, /* 1 */
283 { "secdownload.uri-prefix", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION }, /* 2 */
284 { "secdownload.timeout", NULL, T_CONFIG_INT, T_CONFIG_SCOPE_CONNECTION }, /* 3 */
285 { "secdownload.algorithm", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION }, /* 4 */
286 { "secdownload.path-segments", NULL, T_CONFIG_SHORT, T_CONFIG_SCOPE_CONNECTION }, /* 5 */
287 { "secdownload.hash-querystr", NULL, T_CONFIG_BOOLEAN,T_CONFIG_SCOPE_CONNECTION }, /* 6 */
288 { NULL, NULL, T_CONFIG_UNSET, T_CONFIG_SCOPE_UNSET }
291 if (!p) return HANDLER_ERROR;
293 p->config_storage = calloc(1, srv->config_context->used * sizeof(plugin_config *));
295 for (i = 0; i < srv->config_context->used; i++) {
296 data_config const* config = (data_config const*)srv->config_context->data[i];
297 plugin_config *s;
298 buffer *algorithm = buffer_init();
300 s = calloc(1, sizeof(plugin_config));
301 s->secret = buffer_init();
302 s->doc_root = buffer_init();
303 s->uri_prefix = buffer_init();
304 s->timeout = 60;
305 s->path_segments = 0;
306 s->hash_querystr = 0;
308 cv[0].destination = s->secret;
309 cv[1].destination = s->doc_root;
310 cv[2].destination = s->uri_prefix;
311 cv[3].destination = &(s->timeout);
312 cv[4].destination = algorithm;
313 cv[5].destination = &(s->path_segments);
314 cv[6].destination = &(s->hash_querystr);
316 p->config_storage[i] = s;
318 if (0 != config_insert_values_global(srv, config->value, cv, i == 0 ? T_CONFIG_SCOPE_SERVER : T_CONFIG_SCOPE_CONNECTION)) {
319 buffer_free(algorithm);
320 return HANDLER_ERROR;
323 if (!buffer_is_empty(algorithm)) {
324 s->algorithm = algorithm_from_string(algorithm);
325 switch (s->algorithm) {
326 case SECDL_INVALID:
327 log_error_write(srv, __FILE__, __LINE__, "sb",
328 "invalid secdownload.algorithm:",
329 algorithm);
330 buffer_free(algorithm);
331 return HANDLER_ERROR;
332 #ifndef USE_OPENSSL_CRYPTO
333 case SECDL_HMAC_SHA1:
334 case SECDL_HMAC_SHA256:
335 log_error_write(srv, __FILE__, __LINE__, "sb",
336 "unsupported secdownload.algorithm:",
337 algorithm);
338 #endif
339 default:
340 break;
344 buffer_free(algorithm);
347 return HANDLER_GO_ON;
351 * checks if the supplied string is a hex string
353 * @param str a possible hex string
354 * @return if the supplied string is a valid hex string 1 is returned otherwise 0
357 static int is_hex_len(const char *str, size_t len) {
358 size_t i;
360 if (NULL == str) return 0;
362 for (i = 0; i < len && *str; i++, str++) {
363 /* illegal characters */
364 if (!((*str >= '0' && *str <= '9') ||
365 (*str >= 'a' && *str <= 'f') ||
366 (*str >= 'A' && *str <= 'F'))
368 return 0;
372 return i == len;
376 * checks if the supplied string is a base64 (modified URL) string
378 * @param str a possible base64 (modified URL) string
379 * @return if the supplied string is a valid base64 (modified URL) string 1 is returned otherwise 0
382 static int is_base64_len(const char *str, size_t len) {
383 size_t i;
385 if (NULL == str) return 0;
387 for (i = 0; i < len && *str; i++, str++) {
388 /* illegal characters */
389 if (!((*str >= '0' && *str <= '9') ||
390 (*str >= 'a' && *str <= 'z') ||
391 (*str >= 'A' && *str <= 'Z') ||
392 (*str == '-') || (*str == '_'))
394 return 0;
398 return i == len;
401 #define PATCH(x) \
402 p->conf.x = s->x;
403 static int mod_secdownload_patch_connection(server *srv, connection *con, plugin_data *p) {
404 size_t i, j;
405 plugin_config *s = p->config_storage[0];
407 PATCH(secret);
408 PATCH(doc_root);
409 PATCH(uri_prefix);
410 PATCH(timeout);
411 PATCH(algorithm);
412 PATCH(path_segments);
413 PATCH(hash_querystr);
415 /* skip the first, the global context */
416 for (i = 1; i < srv->config_context->used; i++) {
417 data_config *dc = (data_config *)srv->config_context->data[i];
418 s = p->config_storage[i];
420 /* condition didn't match */
421 if (!config_check_cond(srv, con, dc)) continue;
423 /* merge config */
424 for (j = 0; j < dc->value->used; j++) {
425 data_unset *du = dc->value->data[j];
427 if (buffer_is_equal_string(du->key, CONST_STR_LEN("secdownload.secret"))) {
428 PATCH(secret);
429 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("secdownload.document-root"))) {
430 PATCH(doc_root);
431 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("secdownload.uri-prefix"))) {
432 PATCH(uri_prefix);
433 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("secdownload.timeout"))) {
434 PATCH(timeout);
435 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("secdownload.algorithm"))) {
436 PATCH(algorithm);
437 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("secdownload.path-segments"))) {
438 PATCH(path_segments);
439 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("secdownload.hash-querystr"))) {
440 PATCH(hash_querystr);
445 return 0;
447 #undef PATCH
450 URIHANDLER_FUNC(mod_secdownload_uri_handler) {
451 plugin_data *p = p_d;
452 const char *rel_uri, *ts_str, *mac_str, *protected_path;
453 time_t ts = 0;
454 size_t i, mac_len;
456 if (con->mode != DIRECT) return HANDLER_GO_ON;
458 if (buffer_is_empty(con->uri.path)) return HANDLER_GO_ON;
460 mod_secdownload_patch_connection(srv, con, p);
462 if (buffer_string_is_empty(p->conf.uri_prefix)) return HANDLER_GO_ON;
464 if (buffer_string_is_empty(p->conf.secret)) {
465 log_error_write(srv, __FILE__, __LINE__, "s",
466 "secdownload.secret has to be set");
467 con->http_status = 500;
468 return HANDLER_FINISHED;
471 if (buffer_string_is_empty(p->conf.doc_root)) {
472 log_error_write(srv, __FILE__, __LINE__, "s",
473 "secdownload.document-root has to be set");
474 con->http_status = 500;
475 return HANDLER_FINISHED;
478 if (SECDL_INVALID == p->conf.algorithm) {
479 log_error_write(srv, __FILE__, __LINE__, "s",
480 "secdownload.algorithm has to be set");
481 con->http_status = 500;
482 return HANDLER_FINISHED;
485 mac_len = secdl_algorithm_mac_length(p->conf.algorithm);
487 if (0 != strncmp(con->uri.path->ptr, p->conf.uri_prefix->ptr, buffer_string_length(p->conf.uri_prefix))) return HANDLER_GO_ON;
489 mac_str = con->uri.path->ptr + buffer_string_length(p->conf.uri_prefix);
491 if (!is_base64_len(mac_str, mac_len)) return HANDLER_GO_ON;
493 protected_path = mac_str + mac_len;
494 if (*protected_path != '/') return HANDLER_GO_ON;
496 ts_str = protected_path + 1;
497 if (!is_hex_len(ts_str, 8)) return HANDLER_GO_ON;
498 if (*(ts_str + 8) != '/') return HANDLER_GO_ON;
500 for (i = 0; i < 8; i++) {
501 ts = (ts << 4) + hex2int(ts_str[i]);
504 /* timed-out */
505 if ( (srv->cur_ts > ts && (unsigned int) (srv->cur_ts - ts) > p->conf.timeout) ||
506 (srv->cur_ts < ts && (unsigned int) (ts - srv->cur_ts) > p->conf.timeout) ) {
507 /* "Gone" as the url will never be valid again instead of "408 - Timeout" where the request may be repeated */
508 con->http_status = 410;
510 return HANDLER_FINISHED;
513 rel_uri = ts_str + 8;
515 if (p->conf.path_segments) {
516 const char *rel_uri_end = rel_uri;
517 unsigned int count = p->conf.path_segments;
518 do {
519 rel_uri_end = strchr(rel_uri_end+1, '/');
520 } while (rel_uri_end && --count);
521 if (rel_uri_end) {
522 buffer_copy_string_len(srv->tmp_buf, protected_path,
523 rel_uri_end - protected_path);
524 protected_path = srv->tmp_buf->ptr;
528 if (p->conf.hash_querystr && !buffer_is_empty(con->uri.query)) {
529 buffer *b = srv->tmp_buf;
530 if (protected_path != b->ptr) {
531 buffer_copy_string(b, protected_path);
533 buffer_append_string_len(b, CONST_STR_LEN("?"));
534 buffer_append_string_buffer(b, con->uri.query);
535 /* assign last in case b->ptr is reallocated */
536 protected_path = b->ptr;
539 if (!secdl_verify_mac(srv, &p->conf, protected_path, mac_str, mac_len)) {
540 con->http_status = 403;
542 if (con->conf.log_request_handling) {
543 log_error_write(srv, __FILE__, __LINE__, "sb",
544 "mac invalid:",
545 con->uri.path);
548 return HANDLER_FINISHED;
551 /* starting with the last / we should have relative-path to the docroot
554 buffer_copy_buffer(con->physical.doc_root, p->conf.doc_root);
555 buffer_copy_buffer(con->physical.basedir, p->conf.doc_root);
556 buffer_copy_string(con->physical.rel_path, rel_uri);
557 buffer_copy_buffer(con->physical.path, con->physical.doc_root);
558 buffer_append_string_buffer(con->physical.path, con->physical.rel_path);
560 return HANDLER_GO_ON;
563 /* this function is called at dlopen() time and inits the callbacks */
565 int mod_secdownload_plugin_init(plugin *p);
566 int mod_secdownload_plugin_init(plugin *p) {
567 p->version = LIGHTTPD_VERSION_ID;
568 p->name = buffer_init_string("secdownload");
570 p->init = mod_secdownload_init;
571 p->handle_physical = mod_secdownload_uri_handler;
572 p->set_defaults = mod_secdownload_set_defaults;
573 p->cleanup = mod_secdownload_free;
575 p->data = NULL;
577 return 0;