Annotation of embedaddon/lighttpd/src/mod_secdownload.c, revision 1.1.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>