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