13 #if defined HAVE_LIBSSL && defined HAVE_OPENSSL_SSL_H
14 #define USE_OPENSSL_CRYPTO
17 #ifdef USE_OPENSSL_CRYPTO
18 #include <openssl/evp.h>
19 #include <openssl/hmac.h>
25 typedef unsigned char HASH
[HASHLEN
];
27 typedef char HASHHEX
[HASHHEXLEN
+1];
30 * mod_secdownload verifies a checksum associated with a timestamp
33 * It takes an URL of the form:
34 * securl := <uri-prefix> <mac> <protected-path>
35 * uri-prefix := '/' any* # whatever was configured: must start with a '/')
36 * mac := [a-zA-Z0-9_-]{mac_len} # mac length depends on selected algorithm
37 * protected-path := '/' <timestamp> <rel-path>
38 * timestamp := [a-f0-9]{8} # timestamp when the checksum was calculated
39 * # to prevent access after timeout (active requests
40 * # will finish successfully even after the timeout)
41 * rel-path := '/' any* # the protected path; changing the path breaks the
44 * The timestamp is the `epoch` timestamp in hex, i.e. time in seconds
45 * since 00:00:00 UTC on 1 January 1970.
47 * mod_secdownload supports various MAC algorithms:
50 * mac_len := 32 (and hex only)
51 * mac := md5-hex(<secrect><rel-path><timestamp>) # lowercase hex
53 use Digest::MD5 qw(md5_hex);
54 my $secret = "verysecret";
55 my $rel_path = "/index.html"
56 my $xtime = sprintf("%08x", time);
57 my $url = '/'. md5_hex($secret . $rel_path . $xtime) . '/' . $xtime . $rel_path;
60 * mac_len := 27 (no base64 padding)
61 * mac := base64-url(hmac-sha1(<secret>, <protected-path>))
63 use Digest::SHA qw(hmac_sha1);
64 use MIME::Base64 qw(encode_base64url);
65 my $secret = "verysecret";
66 my $rel_path = "/index.html"
67 my $protected_path = '/' . sprintf("%08x", time) . $rel_path;
68 my $url = '/'. encode_base64url(hmac_sha1($protected_path, $secret)) . $protected_path;
71 * mac_len := 43 (no base64 padding)
72 * mac := base64-url(hmac-256(<secret>, <protected-path>))
73 use Digest::SHA qw(hmac_sha256);
74 use MIME::Base64 qw(encode_base64url);
75 my $secret = "verysecret";
76 my $rel_path = "/index.html"
77 my $protected_path = '/' . sprintf("%08x", time) . $rel_path;
78 my $url = '/'. encode_base64url(hmac_sha256($protected_path, $secret)) . $protected_path;
82 /* plugin config for all request/connections */
88 SECDL_HMAC_SHA256
= 3,
95 secdl_algorithm algorithm
;
98 unsigned short path_segments
;
99 unsigned short hash_querystr
;
105 plugin_config
**config_storage
;
110 static int const_time_memeq(const char *a
, const char *b
, size_t len
) {
111 /* constant time memory compare, unless the compiler figures it out */
114 for (i
= 0; i
< len
; ++i
) {
115 diff
|= (a
[i
] ^ b
[i
]);
120 static const char* secdl_algorithm_names
[] = {
127 static secdl_algorithm
algorithm_from_string(buffer
*name
) {
130 if (buffer_string_is_empty(name
)) return SECDL_INVALID
;
132 for (ndx
= 1; ndx
< sizeof(secdl_algorithm_names
)/sizeof(secdl_algorithm_names
[0]); ++ndx
) {
133 if (0 == strcmp(secdl_algorithm_names
[ndx
], name
->ptr
)) return (secdl_algorithm
)ndx
;
136 return SECDL_INVALID
;
139 static size_t secdl_algorithm_mac_length(secdl_algorithm alg
) {
145 case SECDL_HMAC_SHA1
:
147 case SECDL_HMAC_SHA256
:
153 static int secdl_verify_mac(server
*srv
, plugin_config
*config
, const char* protected_path
, const char* mac
, size_t maclen
) {
155 if (0 == maclen
|| secdl_algorithm_mac_length(config
->algorithm
) != maclen
) return 0;
157 switch (config
->algorithm
) {
169 * protected_path := '/' <timestamp-hex> <rel-path>
170 * timestamp-hex := [0-9a-f]{8}
171 * rel-path := '/' any*
172 * (the protected path was already verified)
173 * message = <secret><rel-path><timestamp-hex>
175 ts_str
= protected_path
+ 1;
176 rel_uri
= ts_str
+ 8;
178 li_MD5_Init(&Md5Ctx
);
179 li_MD5_Update(&Md5Ctx
, CONST_BUF_LEN(config
->secret
));
180 li_MD5_Update(&Md5Ctx
, rel_uri
, strlen(rel_uri
));
181 li_MD5_Update(&Md5Ctx
, ts_str
, 8);
182 li_MD5_Final(HA1
, &Md5Ctx
);
184 li_tohex(hexmd5
, sizeof(hexmd5
), (const char *)HA1
, 16);
186 return (32 == maclen
) && const_time_memeq(mac
, hexmd5
, 32);
188 case SECDL_HMAC_SHA1
:
189 #ifdef USE_OPENSSL_CRYPTO
191 unsigned char digest
[20];
192 char base64_digest
[27];
196 (unsigned char const*) CONST_BUF_LEN(config
->secret
),
197 (unsigned char const*) protected_path
, strlen(protected_path
),
199 log_error_write(srv
, __FILE__
, __LINE__
, "s",
200 "hmac-sha1: HMAC() failed");
204 li_to_base64_no_padding(base64_digest
, 27, digest
, 20, BASE64_URL
);
206 return (27 == maclen
) && const_time_memeq(mac
, base64_digest
, 27);
210 case SECDL_HMAC_SHA256
:
211 #ifdef USE_OPENSSL_CRYPTO
213 unsigned char digest
[32];
214 char base64_digest
[43];
218 (unsigned char const*) CONST_BUF_LEN(config
->secret
),
219 (unsigned char const*) protected_path
, strlen(protected_path
),
221 log_error_write(srv
, __FILE__
, __LINE__
, "s",
222 "hmac-sha256: HMAC() failed");
226 li_to_base64_no_padding(base64_digest
, 43, digest
, 32, BASE64_URL
);
228 return (43 == maclen
) && const_time_memeq(mac
, base64_digest
, 43);
237 /* init the plugin data */
238 INIT_FUNC(mod_secdownload_init
) {
241 p
= calloc(1, sizeof(*p
));
246 /* detroy the plugin data */
247 FREE_FUNC(mod_secdownload_free
) {
248 plugin_data
*p
= p_d
;
251 if (!p
) return HANDLER_GO_ON
;
253 if (p
->config_storage
) {
255 for (i
= 0; i
< srv
->config_context
->used
; i
++) {
256 plugin_config
*s
= p
->config_storage
[i
];
258 if (NULL
== s
) continue;
260 buffer_free(s
->secret
);
261 buffer_free(s
->doc_root
);
262 buffer_free(s
->uri_prefix
);
266 free(p
->config_storage
);
271 return HANDLER_GO_ON
;
274 /* handle plugin config and check values */
276 SETDEFAULTS_FUNC(mod_secdownload_set_defaults
) {
277 plugin_data
*p
= p_d
;
280 config_values_t cv
[] = {
281 { "secdownload.secret", NULL
, T_CONFIG_STRING
, T_CONFIG_SCOPE_CONNECTION
}, /* 0 */
282 { "secdownload.document-root", NULL
, T_CONFIG_STRING
, T_CONFIG_SCOPE_CONNECTION
}, /* 1 */
283 { "secdownload.uri-prefix", NULL
, T_CONFIG_STRING
, T_CONFIG_SCOPE_CONNECTION
}, /* 2 */
284 { "secdownload.timeout", NULL
, T_CONFIG_INT
, T_CONFIG_SCOPE_CONNECTION
}, /* 3 */
285 { "secdownload.algorithm", NULL
, T_CONFIG_STRING
, T_CONFIG_SCOPE_CONNECTION
}, /* 4 */
286 { "secdownload.path-segments", NULL
, T_CONFIG_SHORT
, T_CONFIG_SCOPE_CONNECTION
}, /* 5 */
287 { "secdownload.hash-querystr", NULL
, T_CONFIG_BOOLEAN
,T_CONFIG_SCOPE_CONNECTION
}, /* 6 */
288 { NULL
, NULL
, T_CONFIG_UNSET
, T_CONFIG_SCOPE_UNSET
}
291 if (!p
) return HANDLER_ERROR
;
293 p
->config_storage
= calloc(1, srv
->config_context
->used
* sizeof(plugin_config
*));
295 for (i
= 0; i
< srv
->config_context
->used
; i
++) {
296 data_config
const* config
= (data_config
const*)srv
->config_context
->data
[i
];
298 buffer
*algorithm
= buffer_init();
300 s
= calloc(1, sizeof(plugin_config
));
301 s
->secret
= buffer_init();
302 s
->doc_root
= buffer_init();
303 s
->uri_prefix
= buffer_init();
305 s
->path_segments
= 0;
306 s
->hash_querystr
= 0;
308 cv
[0].destination
= s
->secret
;
309 cv
[1].destination
= s
->doc_root
;
310 cv
[2].destination
= s
->uri_prefix
;
311 cv
[3].destination
= &(s
->timeout
);
312 cv
[4].destination
= algorithm
;
313 cv
[5].destination
= &(s
->path_segments
);
314 cv
[6].destination
= &(s
->hash_querystr
);
316 p
->config_storage
[i
] = s
;
318 if (0 != config_insert_values_global(srv
, config
->value
, cv
, i
== 0 ? T_CONFIG_SCOPE_SERVER
: T_CONFIG_SCOPE_CONNECTION
)) {
319 buffer_free(algorithm
);
320 return HANDLER_ERROR
;
323 if (!buffer_is_empty(algorithm
)) {
324 s
->algorithm
= algorithm_from_string(algorithm
);
325 switch (s
->algorithm
) {
327 log_error_write(srv
, __FILE__
, __LINE__
, "sb",
328 "invalid secdownload.algorithm:",
330 buffer_free(algorithm
);
331 return HANDLER_ERROR
;
332 #ifndef USE_OPENSSL_CRYPTO
333 case SECDL_HMAC_SHA1
:
334 case SECDL_HMAC_SHA256
:
335 log_error_write(srv
, __FILE__
, __LINE__
, "sb",
336 "unsupported secdownload.algorithm:",
344 buffer_free(algorithm
);
347 return HANDLER_GO_ON
;
351 * checks if the supplied string is a hex string
353 * @param str a possible hex string
354 * @return if the supplied string is a valid hex string 1 is returned otherwise 0
357 static int is_hex_len(const char *str
, size_t len
) {
360 if (NULL
== str
) return 0;
362 for (i
= 0; i
< len
&& *str
; i
++, str
++) {
363 /* illegal characters */
364 if (!((*str
>= '0' && *str
<= '9') ||
365 (*str
>= 'a' && *str
<= 'f') ||
366 (*str
>= 'A' && *str
<= 'F'))
376 * checks if the supplied string is a base64 (modified URL) string
378 * @param str a possible base64 (modified URL) string
379 * @return if the supplied string is a valid base64 (modified URL) string 1 is returned otherwise 0
382 static int is_base64_len(const char *str
, size_t len
) {
385 if (NULL
== str
) return 0;
387 for (i
= 0; i
< len
&& *str
; i
++, str
++) {
388 /* illegal characters */
389 if (!((*str
>= '0' && *str
<= '9') ||
390 (*str
>= 'a' && *str
<= 'z') ||
391 (*str
>= 'A' && *str
<= 'Z') ||
392 (*str
== '-') || (*str
== '_'))
403 static int mod_secdownload_patch_connection(server
*srv
, connection
*con
, plugin_data
*p
) {
405 plugin_config
*s
= p
->config_storage
[0];
412 PATCH(path_segments
);
413 PATCH(hash_querystr
);
415 /* skip the first, the global context */
416 for (i
= 1; i
< srv
->config_context
->used
; i
++) {
417 data_config
*dc
= (data_config
*)srv
->config_context
->data
[i
];
418 s
= p
->config_storage
[i
];
420 /* condition didn't match */
421 if (!config_check_cond(srv
, con
, dc
)) continue;
424 for (j
= 0; j
< dc
->value
->used
; j
++) {
425 data_unset
*du
= dc
->value
->data
[j
];
427 if (buffer_is_equal_string(du
->key
, CONST_STR_LEN("secdownload.secret"))) {
429 } else if (buffer_is_equal_string(du
->key
, CONST_STR_LEN("secdownload.document-root"))) {
431 } else if (buffer_is_equal_string(du
->key
, CONST_STR_LEN("secdownload.uri-prefix"))) {
433 } else if (buffer_is_equal_string(du
->key
, CONST_STR_LEN("secdownload.timeout"))) {
435 } else if (buffer_is_equal_string(du
->key
, CONST_STR_LEN("secdownload.algorithm"))) {
437 } else if (buffer_is_equal_string(du
->key
, CONST_STR_LEN("secdownload.path-segments"))) {
438 PATCH(path_segments
);
439 } else if (buffer_is_equal_string(du
->key
, CONST_STR_LEN("secdownload.hash-querystr"))) {
440 PATCH(hash_querystr
);
450 URIHANDLER_FUNC(mod_secdownload_uri_handler
) {
451 plugin_data
*p
= p_d
;
452 const char *rel_uri
, *ts_str
, *mac_str
, *protected_path
;
456 if (con
->mode
!= DIRECT
) return HANDLER_GO_ON
;
458 if (buffer_is_empty(con
->uri
.path
)) return HANDLER_GO_ON
;
460 mod_secdownload_patch_connection(srv
, con
, p
);
462 if (buffer_string_is_empty(p
->conf
.uri_prefix
)) return HANDLER_GO_ON
;
464 if (buffer_string_is_empty(p
->conf
.secret
)) {
465 log_error_write(srv
, __FILE__
, __LINE__
, "s",
466 "secdownload.secret has to be set");
467 con
->http_status
= 500;
468 return HANDLER_FINISHED
;
471 if (buffer_string_is_empty(p
->conf
.doc_root
)) {
472 log_error_write(srv
, __FILE__
, __LINE__
, "s",
473 "secdownload.document-root has to be set");
474 con
->http_status
= 500;
475 return HANDLER_FINISHED
;
478 if (SECDL_INVALID
== p
->conf
.algorithm
) {
479 log_error_write(srv
, __FILE__
, __LINE__
, "s",
480 "secdownload.algorithm has to be set");
481 con
->http_status
= 500;
482 return HANDLER_FINISHED
;
485 mac_len
= secdl_algorithm_mac_length(p
->conf
.algorithm
);
487 if (0 != strncmp(con
->uri
.path
->ptr
, p
->conf
.uri_prefix
->ptr
, buffer_string_length(p
->conf
.uri_prefix
))) return HANDLER_GO_ON
;
489 mac_str
= con
->uri
.path
->ptr
+ buffer_string_length(p
->conf
.uri_prefix
);
491 if (!is_base64_len(mac_str
, mac_len
)) return HANDLER_GO_ON
;
493 protected_path
= mac_str
+ mac_len
;
494 if (*protected_path
!= '/') return HANDLER_GO_ON
;
496 ts_str
= protected_path
+ 1;
497 if (!is_hex_len(ts_str
, 8)) return HANDLER_GO_ON
;
498 if (*(ts_str
+ 8) != '/') return HANDLER_GO_ON
;
500 for (i
= 0; i
< 8; i
++) {
501 ts
= (ts
<< 4) + hex2int(ts_str
[i
]);
505 if ( (srv
->cur_ts
> ts
&& (unsigned int) (srv
->cur_ts
- ts
) > p
->conf
.timeout
) ||
506 (srv
->cur_ts
< ts
&& (unsigned int) (ts
- srv
->cur_ts
) > p
->conf
.timeout
) ) {
507 /* "Gone" as the url will never be valid again instead of "408 - Timeout" where the request may be repeated */
508 con
->http_status
= 410;
510 return HANDLER_FINISHED
;
513 rel_uri
= ts_str
+ 8;
515 if (p
->conf
.path_segments
) {
516 const char *rel_uri_end
= rel_uri
;
517 unsigned int count
= p
->conf
.path_segments
;
519 rel_uri_end
= strchr(rel_uri_end
+1, '/');
520 } while (rel_uri_end
&& --count
);
522 buffer_copy_string_len(srv
->tmp_buf
, protected_path
,
523 rel_uri_end
- protected_path
);
524 protected_path
= srv
->tmp_buf
->ptr
;
528 if (p
->conf
.hash_querystr
&& !buffer_is_empty(con
->uri
.query
)) {
529 buffer
*b
= srv
->tmp_buf
;
530 if (protected_path
!= b
->ptr
) {
531 buffer_copy_string(b
, protected_path
);
533 buffer_append_string_len(b
, CONST_STR_LEN("?"));
534 buffer_append_string_buffer(b
, con
->uri
.query
);
535 /* assign last in case b->ptr is reallocated */
536 protected_path
= b
->ptr
;
539 if (!secdl_verify_mac(srv
, &p
->conf
, protected_path
, mac_str
, mac_len
)) {
540 con
->http_status
= 403;
542 if (con
->conf
.log_request_handling
) {
543 log_error_write(srv
, __FILE__
, __LINE__
, "sb",
548 return HANDLER_FINISHED
;
551 /* starting with the last / we should have relative-path to the docroot
554 buffer_copy_buffer(con
->physical
.doc_root
, p
->conf
.doc_root
);
555 buffer_copy_buffer(con
->physical
.basedir
, p
->conf
.doc_root
);
556 buffer_copy_string(con
->physical
.rel_path
, rel_uri
);
557 buffer_copy_buffer(con
->physical
.path
, con
->physical
.doc_root
);
558 buffer_append_string_buffer(con
->physical
.path
, con
->physical
.rel_path
);
560 return HANDLER_GO_ON
;
563 /* this function is called at dlopen() time and inits the callbacks */
565 int mod_secdownload_plugin_init(plugin
*p
);
566 int mod_secdownload_plugin_init(plugin
*p
) {
567 p
->version
= LIGHTTPD_VERSION_ID
;
568 p
->name
= buffer_init_string("secdownload");
570 p
->init
= mod_secdownload_init
;
571 p
->handle_physical
= mod_secdownload_uri_handler
;
572 p
->set_defaults
= mod_secdownload_set_defaults
;
573 p
->cleanup
= mod_secdownload_free
;