1#include "base.h" 2#include "log.h" 3#include "buffer.h" 4#include "base64.h" 5 6#include "plugin.h" 7 8#include <ctype.h> 9#include <stdlib.h> 10#include <string.h> 11 12#if defined(USE_OPENSSL) 13#include <openssl/evp.h> 14#include <openssl/hmac.h> 15#endif 16 17#include "md5.h" 18 19#define HASHLEN 16 20typedef unsigned char HASH[HASHLEN]; 21#define HASHHEXLEN 32 22typedef char HASHHEX[HASHHEXLEN+1]; 23 24/* 25 * mod_secdownload verifies a checksum associated with a timestamp 26 * and a path. 27 * 28 * It takes an URL of the form: 29 * securl := <uri-prefix> <mac> <protected-path> 30 * uri-prefix := '/' any* # whatever was configured: must start with a '/') 31 * mac := [a-zA-Z0-9_-]{mac_len} # mac length depends on selected algorithm 32 * protected-path := '/' <timestamp> <rel-path> 33 * timestamp := [a-f0-9]{8} # timestamp when the checksum was calculated 34 * # to prevent access after timeout (active requests 35 * # will finish successfully even after the timeout) 36 * rel-path := '/' any* # the protected path; changing the path breaks the 37 * # checksum 38 * 39 * The timestamp is the `epoch` timestamp in hex, i.e. time in seconds 40 * since 00:00:00 UTC on 1 January 1970. 41 * 42 * mod_secdownload supports various MAC algorithms: 43 * 44 * # md5 45 * mac_len := 32 (and hex only) 46 * mac := md5-hex(<secrect><rel-path><timestamp>) # lowercase hex 47 * perl example: 48 use Digest::MD5 qw(md5_hex); 49 my $secret = "verysecret"; 50 my $rel_path = "/index.html" 51 my $xtime = sprintf("%08x", time); 52 my $url = '/'. md5_hex($secret . $rel_path . $xtime) . '/' . $xtime . $rel_path; 53 * 54 * # hmac-sha1 55 * mac_len := 27 (no base64 padding) 56 * mac := base64-url(hmac-sha1(<secret>, <protected-path>)) 57 * perl example: 58 use Digest::SHA qw(hmac_sha1); 59 use MIME::Base64 qw(encode_base64url); 60 my $secret = "verysecret"; 61 my $rel_path = "/index.html" 62 my $protected_path = '/' . sprintf("%08x", time) . $rel_path; 63 my $url = '/'. encode_base64url(hmac_sha1($protected_path, $secret)) . $protected_path; 64 * 65 * # hmac-256 66 * mac_len := 43 (no base64 padding) 67 * mac := base64-url(hmac-256(<secret>, <protected-path>)) 68 use Digest::SHA qw(hmac_sha256); 69 use MIME::Base64 qw(encode_base64url); 70 my $secret = "verysecret"; 71 my $rel_path = "/index.html" 72 my $protected_path = '/' . sprintf("%08x", time) . $rel_path; 73 my $url = '/'. encode_base64url(hmac_sha256($protected_path, $secret)) . $protected_path; 74 * 75 */ 76 77/* plugin config for all request/connections */ 78 79typedef enum { 80 SECDL_INVALID = 0, 81 SECDL_MD5 = 1, 82 SECDL_HMAC_SHA1 = 2, 83 SECDL_HMAC_SHA256 = 3, 84} secdl_algorithm; 85 86typedef struct { 87 buffer *doc_root; 88 buffer *secret; 89 buffer *uri_prefix; 90 secdl_algorithm algorithm; 91 92 unsigned int timeout; 93} plugin_config; 94 95typedef struct { 96 PLUGIN_DATA; 97 98 plugin_config **config_storage; 99 100 plugin_config conf; 101} plugin_data; 102 103static 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]); 109 } 110 return 0 == diff; 111} 112 113static const char* secdl_algorithm_names[] = { 114 "invalid", 115 "md5", 116 "hmac-sha1", 117 "hmac-sha256", 118}; 119 120static secdl_algorithm algorithm_from_string(buffer *name) { 121 size_t ndx; 122 123 if (buffer_string_is_empty(name)) return SECDL_INVALID; 124 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; 127 } 128 129 return SECDL_INVALID; 130} 131 132static 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; 142 } 143 return 0; 144} 145 146static int secdl_verify_mac(server *srv, plugin_config *config, const char* protected_path, const char* mac, size_t maclen) { 147 if (0 == maclen || secdl_algorithm_mac_length(config->algorithm) != maclen) return 0; 148 149 switch (config->algorithm) { 150 case SECDL_INVALID: 151 break; 152 case SECDL_MD5: 153 { 154 li_MD5_CTX Md5Ctx; 155 HASH HA1; 156 char hexmd5[32]; 157 const char *ts_str; 158 const char *rel_uri; 159 160 /* legacy message: 161 * protected_path := '/' <timestamp-hex> <rel-path> 162 * timestamp-hex := [0-9a-f]{8} 163 * rel-path := '/' any* 164 * (the protected path was already verified) 165 * message = <secret><rel-path><timestamp-hex> 166 */ 167 ts_str = protected_path + 1; 168 rel_uri = ts_str + 8; 169 170 li_MD5_Init(&Md5Ctx); 171 li_MD5_Update(&Md5Ctx, CONST_BUF_LEN(config->secret)); 172 li_MD5_Update(&Md5Ctx, rel_uri, strlen(rel_uri)); 173 li_MD5_Update(&Md5Ctx, ts_str, 8); 174 li_MD5_Final(HA1, &Md5Ctx); 175 176 li_tohex(hexmd5, (const char *)HA1, 16); 177 178 return (32 == maclen) && const_time_memeq(mac, hexmd5, 32); 179 } 180 case SECDL_HMAC_SHA1: 181#if defined(USE_OPENSSL) 182 { 183 unsigned char digest[20]; 184 char base64_digest[27]; 185 186 if (NULL == HMAC( 187 EVP_sha1(), 188 (unsigned char const*) CONST_BUF_LEN(config->secret), 189 (unsigned char const*) protected_path, strlen(protected_path), 190 digest, NULL)) { 191 log_error_write(srv, __FILE__, __LINE__, "s", 192 "hmac-sha1: HMAC() failed"); 193 return 0; 194 } 195 196 li_to_base64_no_padding(base64_digest, 27, digest, 20, BASE64_URL); 197 198 return (27 == maclen) && const_time_memeq(mac, base64_digest, 27); 199 } 200#endif 201 break; 202 case SECDL_HMAC_SHA256: 203#if defined(USE_OPENSSL) 204 { 205 unsigned char digest[32]; 206 char base64_digest[43]; 207 208 if (NULL == HMAC( 209 EVP_sha256(), 210 (unsigned char const*) CONST_BUF_LEN(config->secret), 211 (unsigned char const*) protected_path, strlen(protected_path), 212 digest, NULL)) { 213 log_error_write(srv, __FILE__, __LINE__, "s", 214 "hmac-sha256: HMAC() failed"); 215 return 0; 216 } 217 218 li_to_base64_no_padding(base64_digest, 43, digest, 32, BASE64_URL); 219 220 return (43 == maclen) && const_time_memeq(mac, base64_digest, 43); 221 } 222#endif 223 break; 224 } 225 226 return 0; 227} 228 229/* init the plugin data */ 230INIT_FUNC(mod_secdownload_init) { 231 plugin_data *p; 232 233 p = calloc(1, sizeof(*p)); 234 235 return p; 236} 237 238/* detroy the plugin data */ 239FREE_FUNC(mod_secdownload_free) { 240 plugin_data *p = p_d; 241 UNUSED(srv); 242 243 if (!p) return HANDLER_GO_ON; 244 245 if (p->config_storage) { 246 size_t i; 247 for (i = 0; i < srv->config_context->used; i++) { 248 plugin_config *s = p->config_storage[i]; 249 250 if (NULL == s) continue; 251 252 buffer_free(s->secret); 253 buffer_free(s->doc_root); 254 buffer_free(s->uri_prefix); 255 256 free(s); 257 } 258 free(p->config_storage); 259 } 260 261 free(p); 262 263 return HANDLER_GO_ON; 264} 265 266/* handle plugin config and check values */ 267 268SETDEFAULTS_FUNC(mod_secdownload_set_defaults) { 269 plugin_data *p = p_d; 270 size_t i = 0; 271 272 config_values_t cv[] = { 273 { "secdownload.secret", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION }, /* 0 */ 274 { "secdownload.document-root", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION }, /* 1 */ 275 { "secdownload.uri-prefix", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION }, /* 2 */ 276 { "secdownload.timeout", NULL, T_CONFIG_INT, T_CONFIG_SCOPE_CONNECTION }, /* 3 */ 277 { "secdownload.algorithm", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION }, /* 4 */ 278 { NULL, NULL, T_CONFIG_UNSET, T_CONFIG_SCOPE_UNSET } 279 }; 280 281 if (!p) return HANDLER_ERROR; 282 283 p->config_storage = calloc(1, srv->config_context->used * sizeof(plugin_config *)); 284 285 for (i = 0; i < srv->config_context->used; i++) { 286 data_config const* config = (data_config const*)srv->config_context->data[i]; 287 plugin_config *s; 288 buffer *algorithm = buffer_init(); 289 290 s = calloc(1, sizeof(plugin_config)); 291 s->secret = buffer_init(); 292 s->doc_root = buffer_init(); 293 s->uri_prefix = buffer_init(); 294 s->timeout = 60; 295 296 cv[0].destination = s->secret; 297 cv[1].destination = s->doc_root; 298 cv[2].destination = s->uri_prefix; 299 cv[3].destination = &(s->timeout); 300 cv[4].destination = algorithm; 301 302 p->config_storage[i] = s; 303 304 if (0 != config_insert_values_global(srv, config->value, cv, i == 0 ? T_CONFIG_SCOPE_SERVER : T_CONFIG_SCOPE_CONNECTION)) { 305 buffer_free(algorithm); 306 return HANDLER_ERROR; 307 } 308 309 if (!buffer_is_empty(algorithm)) { 310 s->algorithm = algorithm_from_string(algorithm); 311 switch (s->algorithm) { 312 case SECDL_INVALID: 313 log_error_write(srv, __FILE__, __LINE__, "sb", 314 "invalid secdownload.algorithm:", 315 algorithm); 316 buffer_free(algorithm); 317 return HANDLER_ERROR; 318#if !defined(USE_OPENSSL) 319 case SECDL_HMAC_SHA1: 320 case SECDL_HMAC_SHA256: 321 log_error_write(srv, __FILE__, __LINE__, "sb", 322 "unsupported secdownload.algorithm:", 323 algorithm); 324 buffer_free(algorithm); 325 return HANDLER_ERROR; 326#endif 327 default: 328 break; 329 } 330 } 331 332 buffer_free(algorithm); 333 } 334 335 return HANDLER_GO_ON; 336} 337 338/** 339 * checks if the supplied string is a hex string 340 * 341 * @param str a possible hex string 342 * @return if the supplied string is a valid hex string 1 is returned otherwise 0 343 */ 344 345static int is_hex_len(const char *str, size_t len) { 346 size_t i; 347 348 if (NULL == str) return 0; 349 350 for (i = 0; i < len && *str; i++, str++) { 351 /* illegal characters */ 352 if (!((*str >= '0' && *str <= '9') || 353 (*str >= 'a' && *str <= 'f') || 354 (*str >= 'A' && *str <= 'F')) 355 ) { 356 return 0; 357 } 358 } 359 360 return i == len; 361} 362 363/** 364 * checks if the supplied string is a base64 (modified URL) string 365 * 366 * @param str a possible base64 (modified URL) string 367 * @return if the supplied string is a valid base64 (modified URL) string 1 is returned otherwise 0 368 */ 369 370static int is_base64_len(const char *str, size_t len) { 371 size_t i; 372 373 if (NULL == str) return 0; 374 375 for (i = 0; i < len && *str; i++, str++) { 376 /* illegal characters */ 377 if (!((*str >= '0' && *str <= '9') || 378 (*str >= 'a' && *str <= 'z') || 379 (*str >= 'A' && *str <= 'Z') || 380 (*str == '-') || (*str == '_')) 381 ) { 382 return 0; 383 } 384 } 385 386 return i == len; 387} 388 389#define PATCH(x) \ 390 p->conf.x = s->x; 391static int mod_secdownload_patch_connection(server *srv, connection *con, plugin_data *p) { 392 size_t i, j; 393 plugin_config *s = p->config_storage[0]; 394 395 PATCH(secret); 396 PATCH(doc_root); 397 PATCH(uri_prefix); 398 PATCH(timeout); 399 PATCH(algorithm); 400 401 /* skip the first, the global context */ 402 for (i = 1; i < srv->config_context->used; i++) { 403 data_config *dc = (data_config *)srv->config_context->data[i]; 404 s = p->config_storage[i]; 405 406 /* condition didn't match */ 407 if (!config_check_cond(srv, con, dc)) continue; 408 409 /* merge config */ 410 for (j = 0; j < dc->value->used; j++) { 411 data_unset *du = dc->value->data[j]; 412 413 if (buffer_is_equal_string(du->key, CONST_STR_LEN("secdownload.secret"))) { 414 PATCH(secret); 415 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("secdownload.document-root"))) { 416 PATCH(doc_root); 417 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("secdownload.uri-prefix"))) { 418 PATCH(uri_prefix); 419 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("secdownload.timeout"))) { 420 PATCH(timeout); 421 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("secdownload.algorithm"))) { 422 PATCH(algorithm); 423 } 424 } 425 } 426 427 return 0; 428} 429#undef PATCH 430 431 432URIHANDLER_FUNC(mod_secdownload_uri_handler) { 433 plugin_data *p = p_d; 434 const char *rel_uri, *ts_str, *mac_str, *protected_path; 435 time_t ts = 0; 436 size_t i, mac_len; 437 438 if (con->mode != DIRECT) return HANDLER_GO_ON; 439 440 if (buffer_is_empty(con->uri.path)) return HANDLER_GO_ON; 441 442 mod_secdownload_patch_connection(srv, con, p); 443 444 if (buffer_string_is_empty(p->conf.uri_prefix)) return HANDLER_GO_ON; 445 446 if (buffer_string_is_empty(p->conf.secret)) { 447 log_error_write(srv, __FILE__, __LINE__, "s", 448 "secdownload.secret has to be set"); 449 con->http_status = 500; 450 return HANDLER_FINISHED; 451 } 452 453 if (buffer_string_is_empty(p->conf.doc_root)) { 454 log_error_write(srv, __FILE__, __LINE__, "s", 455 "secdownload.document-root has to be set"); 456 con->http_status = 500; 457 return HANDLER_FINISHED; 458 } 459 460 if (SECDL_INVALID == p->conf.algorithm) { 461 log_error_write(srv, __FILE__, __LINE__, "s", 462 "secdownload.algorithm has to be set"); 463 con->http_status = 500; 464 return HANDLER_FINISHED; 465 } 466 467 mac_len = secdl_algorithm_mac_length(p->conf.algorithm); 468 469 if (0 != strncmp(con->uri.path->ptr, p->conf.uri_prefix->ptr, buffer_string_length(p->conf.uri_prefix))) return HANDLER_GO_ON; 470 471 mac_str = con->uri.path->ptr + buffer_string_length(p->conf.uri_prefix); 472 473 if (!is_base64_len(mac_str, mac_len)) return HANDLER_GO_ON; 474 475 protected_path = mac_str + mac_len; 476 if (*protected_path != '/') return HANDLER_GO_ON; 477 478 ts_str = protected_path + 1; 479 if (!is_hex_len(ts_str, 8)) return HANDLER_GO_ON; 480 if (*(ts_str + 8) != '/') return HANDLER_GO_ON; 481 482 for (i = 0; i < 8; i++) { 483 ts = (ts << 4) + hex2int(ts_str[i]); 484 } 485 486 /* timed-out */ 487 if ( (srv->cur_ts > ts && (unsigned int) (srv->cur_ts - ts) > p->conf.timeout) || 488 (srv->cur_ts < ts && (unsigned int) (ts - srv->cur_ts) > p->conf.timeout) ) { 489 /* "Gone" as the url will never be valid again instead of "408 - Timeout" where the request may be repeated */ 490 con->http_status = 410; 491 492 return HANDLER_FINISHED; 493 } 494 495 rel_uri = ts_str + 8; 496 497 if (!secdl_verify_mac(srv, &p->conf, protected_path, mac_str, mac_len)) { 498 con->http_status = 403; 499 500 if (con->conf.log_request_handling) { 501 log_error_write(srv, __FILE__, __LINE__, "sb", 502 "mac invalid:", 503 con->uri.path); 504 } 505 506 return HANDLER_FINISHED; 507 } 508 509 /* starting with the last / we should have relative-path to the docroot 510 */ 511 512 buffer_copy_buffer(con->physical.doc_root, p->conf.doc_root); 513 buffer_copy_buffer(con->physical.basedir, p->conf.doc_root); 514 buffer_copy_string(con->physical.rel_path, rel_uri); 515 buffer_copy_buffer(con->physical.path, con->physical.doc_root); 516 buffer_append_string_buffer(con->physical.path, con->physical.rel_path); 517 518 return HANDLER_GO_ON; 519} 520 521/* this function is called at dlopen() time and inits the callbacks */ 522 523int mod_secdownload_plugin_init(plugin *p); 524int mod_secdownload_plugin_init(plugin *p) { 525 p->version = LIGHTTPD_VERSION_ID; 526 p->name = buffer_init_string("secdownload"); 527 528 p->init = mod_secdownload_init; 529 p->handle_physical = mod_secdownload_uri_handler; 530 p->set_defaults = mod_secdownload_set_defaults; 531 p->cleanup = mod_secdownload_free; 532 533 p->data = NULL; 534 535 return 0; 536} 537