• Home
  • History
  • Annotate
  • Line#
  • Navigate
  • Raw
  • Download
  • only in /asuswrt-rt-n18u-9.0.0.4.380.2695/release/src-rt-6.x.4708/router/lighttpd-1.4.39/src/
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