[tests] skip mod-secdownload HMAC-SHA1,HMAC-SHA256
[lighttpd.git] / src / mod_secdownload.c
blob28299c65638474f6431123e0fb1f4d56f3d05663
1 #include "first.h"
3 #include "base.h"
4 #include "log.h"
5 #include "buffer.h"
6 #include "base64.h"
7 #include "http_auth.h"
9 #include "plugin.h"
11 #include <stdlib.h>
12 #include <string.h>
14 #include "sys-crypto.h"
15 #ifdef USE_OPENSSL_CRYPTO
16 #include <openssl/evp.h>
17 #include <openssl/hmac.h>
18 #endif
20 #include "md5.h"
23 * mod_secdownload verifies a checksum associated with a timestamp
24 * and a path.
26 * It takes an URL of the form:
27 * securl := <uri-prefix> <mac> <protected-path>
28 * uri-prefix := '/' any* # whatever was configured: must start with a '/')
29 * mac := [a-zA-Z0-9_-]{mac_len} # mac length depends on selected algorithm
30 * protected-path := '/' <timestamp> <rel-path>
31 * timestamp := [a-f0-9]{8} # timestamp when the checksum was calculated
32 * # to prevent access after timeout (active requests
33 * # will finish successfully even after the timeout)
34 * rel-path := '/' any* # the protected path; changing the path breaks the
35 * # checksum
37 * The timestamp is the `epoch` timestamp in hex, i.e. time in seconds
38 * since 00:00:00 UTC on 1 January 1970.
40 * mod_secdownload supports various MAC algorithms:
42 * # md5
43 * mac_len := 32 (and hex only)
44 * mac := md5-hex(<secrect><rel-path><timestamp>) # lowercase hex
45 * perl example:
46 use Digest::MD5 qw(md5_hex);
47 my $secret = "verysecret";
48 my $rel_path = "/index.html"
49 my $xtime = sprintf("%08x", time);
50 my $url = '/'. md5_hex($secret . $rel_path . $xtime) . '/' . $xtime . $rel_path;
52 * # hmac-sha1
53 * mac_len := 27 (no base64 padding)
54 * mac := base64-url(hmac-sha1(<secret>, <protected-path>))
55 * perl example:
56 use Digest::SHA qw(hmac_sha1);
57 use MIME::Base64 qw(encode_base64url);
58 my $secret = "verysecret";
59 my $rel_path = "/index.html"
60 my $protected_path = '/' . sprintf("%08x", time) . $rel_path;
61 my $url = '/'. encode_base64url(hmac_sha1($protected_path, $secret)) . $protected_path;
63 * # hmac-256
64 * mac_len := 43 (no base64 padding)
65 * mac := base64-url(hmac-256(<secret>, <protected-path>))
66 use Digest::SHA qw(hmac_sha256);
67 use MIME::Base64 qw(encode_base64url);
68 my $secret = "verysecret";
69 my $rel_path = "/index.html"
70 my $protected_path = '/' . sprintf("%08x", time) . $rel_path;
71 my $url = '/'. encode_base64url(hmac_sha256($protected_path, $secret)) . $protected_path;
75 /* plugin config for all request/connections */
77 typedef enum {
78 SECDL_INVALID = 0,
79 SECDL_MD5 = 1,
80 SECDL_HMAC_SHA1 = 2,
81 SECDL_HMAC_SHA256 = 3,
82 } secdl_algorithm;
84 typedef struct {
85 buffer *doc_root;
86 buffer *secret;
87 buffer *uri_prefix;
88 secdl_algorithm algorithm;
90 unsigned int timeout;
91 unsigned short path_segments;
92 unsigned short hash_querystr;
93 } plugin_config;
95 typedef struct {
96 PLUGIN_DATA;
98 plugin_config **config_storage;
100 plugin_config conf;
101 } plugin_data;
103 static int const_time_memeq(const char *a, const char *b, size_t len) {
104 /* constant time memory compare, unless the compiler figures it out */
105 char diff = 0;
106 size_t i;
107 for (i = 0; i < len; ++i) {
108 diff |= (a[i] ^ b[i]);
110 return 0 == diff;
113 static const char* secdl_algorithm_names[] = {
114 "invalid",
115 "md5",
116 "hmac-sha1",
117 "hmac-sha256",
120 static secdl_algorithm algorithm_from_string(buffer *name) {
121 size_t ndx;
123 if (buffer_string_is_empty(name)) return SECDL_INVALID;
125 for (ndx = 1; ndx < sizeof(secdl_algorithm_names)/sizeof(secdl_algorithm_names[0]); ++ndx) {
126 if (0 == strcmp(secdl_algorithm_names[ndx], name->ptr)) return (secdl_algorithm)ndx;
129 return SECDL_INVALID;
132 static size_t secdl_algorithm_mac_length(secdl_algorithm alg) {
133 switch (alg) {
134 case SECDL_INVALID:
135 break;
136 case SECDL_MD5:
137 return 32;
138 case SECDL_HMAC_SHA1:
139 return 27;
140 case SECDL_HMAC_SHA256:
141 return 43;
143 return 0;
146 static int secdl_verify_mac(server *srv, plugin_config *config, const char* protected_path, const char* mac, size_t maclen) {
147 UNUSED(srv);
148 if (0 == maclen || secdl_algorithm_mac_length(config->algorithm) != maclen) return 0;
150 switch (config->algorithm) {
151 case SECDL_INVALID:
152 break;
153 case SECDL_MD5:
155 li_MD5_CTX Md5Ctx;
156 const char *ts_str;
157 const char *rel_uri;
158 unsigned char HA1[16];
159 unsigned char md5bin[16];
161 if (0 != http_auth_digest_hex2bin(mac, maclen, md5bin, sizeof(md5bin))) return 0;
163 /* legacy message:
164 * protected_path := '/' <timestamp-hex> <rel-path>
165 * timestamp-hex := [0-9a-f]{8}
166 * rel-path := '/' any*
167 * (the protected path was already verified)
168 * message = <secret><rel-path><timestamp-hex>
170 ts_str = protected_path + 1;
171 rel_uri = ts_str + 8;
173 li_MD5_Init(&Md5Ctx);
174 li_MD5_Update(&Md5Ctx, CONST_BUF_LEN(config->secret));
175 li_MD5_Update(&Md5Ctx, rel_uri, strlen(rel_uri));
176 li_MD5_Update(&Md5Ctx, ts_str, 8);
177 li_MD5_Final(HA1, &Md5Ctx);
179 return const_time_memeq((char *)HA1, (char *)md5bin, sizeof(md5bin));
181 case SECDL_HMAC_SHA1:
182 #ifdef USE_OPENSSL_CRYPTO
184 unsigned char digest[20];
185 char base64_digest[27];
187 if (NULL == HMAC(
188 EVP_sha1(),
189 (unsigned char const*) config->secret->ptr, buffer_string_length(config->secret),
190 (unsigned char const*) protected_path, strlen(protected_path),
191 digest, NULL)) {
192 log_error_write(srv, __FILE__, __LINE__, "s",
193 "hmac-sha1: HMAC() failed");
194 return 0;
197 li_to_base64_no_padding(base64_digest, 27, digest, 20, BASE64_URL);
199 return (27 == maclen) && const_time_memeq(mac, base64_digest, 27);
201 #endif
202 break;
203 case SECDL_HMAC_SHA256:
204 #ifdef USE_OPENSSL_CRYPTO
206 unsigned char digest[32];
207 char base64_digest[43];
209 if (NULL == HMAC(
210 EVP_sha256(),
211 (unsigned char const*) config->secret->ptr, buffer_string_length(config->secret),
212 (unsigned char const*) protected_path, strlen(protected_path),
213 digest, NULL)) {
214 log_error_write(srv, __FILE__, __LINE__, "s",
215 "hmac-sha256: HMAC() failed");
216 return 0;
219 li_to_base64_no_padding(base64_digest, 43, digest, 32, BASE64_URL);
221 return (43 == maclen) && const_time_memeq(mac, base64_digest, 43);
223 #endif
224 break;
227 return 0;
230 /* init the plugin data */
231 INIT_FUNC(mod_secdownload_init) {
232 plugin_data *p;
234 p = calloc(1, sizeof(*p));
236 return p;
239 /* detroy the plugin data */
240 FREE_FUNC(mod_secdownload_free) {
241 plugin_data *p = p_d;
242 UNUSED(srv);
244 if (!p) return HANDLER_GO_ON;
246 if (p->config_storage) {
247 size_t i;
248 for (i = 0; i < srv->config_context->used; i++) {
249 plugin_config *s = p->config_storage[i];
251 if (NULL == s) continue;
253 buffer_free(s->secret);
254 buffer_free(s->doc_root);
255 buffer_free(s->uri_prefix);
257 free(s);
259 free(p->config_storage);
262 free(p);
264 return HANDLER_GO_ON;
267 /* handle plugin config and check values */
269 SETDEFAULTS_FUNC(mod_secdownload_set_defaults) {
270 plugin_data *p = p_d;
271 size_t i = 0;
273 config_values_t cv[] = {
274 { "secdownload.secret", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION }, /* 0 */
275 { "secdownload.document-root", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION }, /* 1 */
276 { "secdownload.uri-prefix", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION }, /* 2 */
277 { "secdownload.timeout", NULL, T_CONFIG_INT, T_CONFIG_SCOPE_CONNECTION }, /* 3 */
278 { "secdownload.algorithm", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION }, /* 4 */
279 { "secdownload.path-segments", NULL, T_CONFIG_SHORT, T_CONFIG_SCOPE_CONNECTION }, /* 5 */
280 { "secdownload.hash-querystr", NULL, T_CONFIG_BOOLEAN,T_CONFIG_SCOPE_CONNECTION }, /* 6 */
281 { NULL, NULL, T_CONFIG_UNSET, T_CONFIG_SCOPE_UNSET }
284 if (!p) return HANDLER_ERROR;
286 p->config_storage = calloc(srv->config_context->used, sizeof(plugin_config *));
288 for (i = 0; i < srv->config_context->used; i++) {
289 data_config const* config = (data_config const*)srv->config_context->data[i];
290 plugin_config *s;
291 buffer *algorithm = buffer_init();
293 s = calloc(1, sizeof(plugin_config));
294 s->secret = buffer_init();
295 s->doc_root = buffer_init();
296 s->uri_prefix = buffer_init();
297 s->timeout = 60;
298 s->path_segments = 0;
299 s->hash_querystr = 0;
301 cv[0].destination = s->secret;
302 cv[1].destination = s->doc_root;
303 cv[2].destination = s->uri_prefix;
304 cv[3].destination = &(s->timeout);
305 cv[4].destination = algorithm;
306 cv[5].destination = &(s->path_segments);
307 cv[6].destination = &(s->hash_querystr);
309 p->config_storage[i] = s;
311 if (0 != config_insert_values_global(srv, config->value, cv, i == 0 ? T_CONFIG_SCOPE_SERVER : T_CONFIG_SCOPE_CONNECTION)) {
312 buffer_free(algorithm);
313 return HANDLER_ERROR;
316 if (!buffer_is_empty(algorithm)) {
317 s->algorithm = algorithm_from_string(algorithm);
318 switch (s->algorithm) {
319 case SECDL_INVALID:
320 log_error_write(srv, __FILE__, __LINE__, "sb",
321 "invalid secdownload.algorithm:",
322 algorithm);
323 buffer_free(algorithm);
324 return HANDLER_ERROR;
325 #ifndef USE_OPENSSL_CRYPTO
326 case SECDL_HMAC_SHA1:
327 case SECDL_HMAC_SHA256:
328 log_error_write(srv, __FILE__, __LINE__, "sb",
329 "unsupported secdownload.algorithm:",
330 algorithm);
331 #endif
332 default:
333 break;
337 buffer_free(algorithm);
340 return HANDLER_GO_ON;
344 * checks if the supplied string is a hex string
346 * @param str a possible hex string
347 * @return if the supplied string is a valid hex string 1 is returned otherwise 0
350 static int is_hex_len(const char *str, size_t len) {
351 size_t i;
353 if (NULL == str) return 0;
355 for (i = 0; i < len && *str; i++, str++) {
356 /* illegal characters */
357 if (!((*str >= '0' && *str <= '9') ||
358 (*str >= 'a' && *str <= 'f') ||
359 (*str >= 'A' && *str <= 'F'))
361 return 0;
365 return i == len;
369 * checks if the supplied string is a base64 (modified URL) string
371 * @param str a possible base64 (modified URL) string
372 * @return if the supplied string is a valid base64 (modified URL) string 1 is returned otherwise 0
375 static int is_base64_len(const char *str, size_t len) {
376 size_t i;
378 if (NULL == str) return 0;
380 for (i = 0; i < len && *str; i++, str++) {
381 /* illegal characters */
382 if (!((*str >= '0' && *str <= '9') ||
383 (*str >= 'a' && *str <= 'z') ||
384 (*str >= 'A' && *str <= 'Z') ||
385 (*str == '-') || (*str == '_'))
387 return 0;
391 return i == len;
394 #define PATCH(x) \
395 p->conf.x = s->x;
396 static int mod_secdownload_patch_connection(server *srv, connection *con, plugin_data *p) {
397 size_t i, j;
398 plugin_config *s = p->config_storage[0];
400 PATCH(secret);
401 PATCH(doc_root);
402 PATCH(uri_prefix);
403 PATCH(timeout);
404 PATCH(algorithm);
405 PATCH(path_segments);
406 PATCH(hash_querystr);
408 /* skip the first, the global context */
409 for (i = 1; i < srv->config_context->used; i++) {
410 data_config *dc = (data_config *)srv->config_context->data[i];
411 s = p->config_storage[i];
413 /* condition didn't match */
414 if (!config_check_cond(srv, con, dc)) continue;
416 /* merge config */
417 for (j = 0; j < dc->value->used; j++) {
418 data_unset *du = dc->value->data[j];
420 if (buffer_is_equal_string(du->key, CONST_STR_LEN("secdownload.secret"))) {
421 PATCH(secret);
422 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("secdownload.document-root"))) {
423 PATCH(doc_root);
424 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("secdownload.uri-prefix"))) {
425 PATCH(uri_prefix);
426 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("secdownload.timeout"))) {
427 PATCH(timeout);
428 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("secdownload.algorithm"))) {
429 PATCH(algorithm);
430 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("secdownload.path-segments"))) {
431 PATCH(path_segments);
432 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("secdownload.hash-querystr"))) {
433 PATCH(hash_querystr);
438 return 0;
440 #undef PATCH
443 URIHANDLER_FUNC(mod_secdownload_uri_handler) {
444 plugin_data *p = p_d;
445 const char *rel_uri, *ts_str, *mac_str, *protected_path;
446 time_t ts = 0;
447 size_t i, mac_len;
449 if (con->mode != DIRECT) return HANDLER_GO_ON;
451 if (buffer_is_empty(con->uri.path)) return HANDLER_GO_ON;
453 mod_secdownload_patch_connection(srv, con, p);
455 if (buffer_string_is_empty(p->conf.uri_prefix)) return HANDLER_GO_ON;
457 if (buffer_string_is_empty(p->conf.secret)) {
458 log_error_write(srv, __FILE__, __LINE__, "s",
459 "secdownload.secret has to be set");
460 con->http_status = 500;
461 return HANDLER_FINISHED;
464 if (buffer_string_is_empty(p->conf.doc_root)) {
465 log_error_write(srv, __FILE__, __LINE__, "s",
466 "secdownload.document-root has to be set");
467 con->http_status = 500;
468 return HANDLER_FINISHED;
471 if (SECDL_INVALID == p->conf.algorithm) {
472 log_error_write(srv, __FILE__, __LINE__, "s",
473 "secdownload.algorithm has to be set");
474 con->http_status = 500;
475 return HANDLER_FINISHED;
478 mac_len = secdl_algorithm_mac_length(p->conf.algorithm);
480 if (0 != strncmp(con->uri.path->ptr, p->conf.uri_prefix->ptr, buffer_string_length(p->conf.uri_prefix))) return HANDLER_GO_ON;
482 mac_str = con->uri.path->ptr + buffer_string_length(p->conf.uri_prefix);
484 if (!is_base64_len(mac_str, mac_len)) return HANDLER_GO_ON;
486 protected_path = mac_str + mac_len;
487 if (*protected_path != '/') return HANDLER_GO_ON;
489 ts_str = protected_path + 1;
490 if (!is_hex_len(ts_str, 8)) return HANDLER_GO_ON;
491 if (*(ts_str + 8) != '/') return HANDLER_GO_ON;
493 for (i = 0; i < 8; i++) {
494 ts = (ts << 4) + hex2int(ts_str[i]);
497 /* timed-out */
498 if ( (srv->cur_ts > ts && (unsigned int) (srv->cur_ts - ts) > p->conf.timeout) ||
499 (srv->cur_ts < ts && (unsigned int) (ts - srv->cur_ts) > p->conf.timeout) ) {
500 /* "Gone" as the url will never be valid again instead of "408 - Timeout" where the request may be repeated */
501 con->http_status = 410;
503 return HANDLER_FINISHED;
506 rel_uri = ts_str + 8;
508 if (p->conf.path_segments) {
509 const char *rel_uri_end = rel_uri;
510 unsigned int count = p->conf.path_segments;
511 do {
512 rel_uri_end = strchr(rel_uri_end+1, '/');
513 } while (rel_uri_end && --count);
514 if (rel_uri_end) {
515 buffer_copy_string_len(srv->tmp_buf, protected_path,
516 rel_uri_end - protected_path);
517 protected_path = srv->tmp_buf->ptr;
521 if (p->conf.hash_querystr && !buffer_is_empty(con->uri.query)) {
522 buffer *b = srv->tmp_buf;
523 if (protected_path != b->ptr) {
524 buffer_copy_string(b, protected_path);
526 buffer_append_string_len(b, CONST_STR_LEN("?"));
527 buffer_append_string_buffer(b, con->uri.query);
528 /* assign last in case b->ptr is reallocated */
529 protected_path = b->ptr;
532 if (!secdl_verify_mac(srv, &p->conf, protected_path, mac_str, mac_len)) {
533 con->http_status = 403;
535 if (con->conf.log_request_handling) {
536 log_error_write(srv, __FILE__, __LINE__, "sb",
537 "mac invalid:",
538 con->uri.path);
541 return HANDLER_FINISHED;
544 /* starting with the last / we should have relative-path to the docroot
547 buffer_copy_buffer(con->physical.doc_root, p->conf.doc_root);
548 buffer_copy_buffer(con->physical.basedir, p->conf.doc_root);
549 buffer_copy_string(con->physical.rel_path, rel_uri);
550 buffer_copy_buffer(con->physical.path, con->physical.doc_root);
551 buffer_append_string_buffer(con->physical.path, con->physical.rel_path);
553 return HANDLER_GO_ON;
556 /* this function is called at dlopen() time and inits the callbacks */
558 int mod_secdownload_plugin_init(plugin *p);
559 int mod_secdownload_plugin_init(plugin *p) {
560 p->version = LIGHTTPD_VERSION_ID;
561 p->name = buffer_init_string("secdownload");
563 p->init = mod_secdownload_init;
564 p->handle_physical = mod_secdownload_uri_handler;
565 p->set_defaults = mod_secdownload_set_defaults;
566 p->cleanup = mod_secdownload_free;
568 p->data = NULL;
570 return 0;