diff --git a/ngx_http_pow.c b/ngx_http_pow.c index 243cd55..c7bd12a 100644 --- a/ngx_http_pow.c +++ b/ngx_http_pow.c @@ -1,22 +1,31 @@ #include #include #include +#include +#include + +#define NGX_HTTP_POW_CHALLENGE_RANDOM_LENGTH 64 +#define NGX_HTTP_POW_HMAC_KEY "thisisaverysecretkey" +#define NGX_HTTP_POW_CHALLENGE_HEADER_KEY "pow-challenge" +#define NGX_HTTP_POW_CHALLENGE_COOKIE_NAME "pow" + +#define NGX_HTTP_POW_UNUSED __attribute__((unused)) static ngx_int_t ngx_http_pow_init(ngx_conf_t *cf); static ngx_int_t ngx_http_pow_handler(ngx_http_request_t *r); static ngx_http_module_t ngx_http_pow_module_ctx = { - NULL, /* preconfiguration */ - ngx_http_pow_init, /* postconfiguration */ + NULL, /* preconfiguration */ + ngx_http_pow_init, /* postconfiguration */ - NULL, /* create main configuration */ - NULL, /* init main configuration */ + NULL, /* create main configuration */ + NULL, /* init main configuration */ - NULL, /* create server configuration */ - NULL, /* merge server configuration */ + NULL, /* create server configuration */ + NULL, /* merge server configuration */ - NULL, /* create location configuration */ - NULL /* merge location configuration */ + NULL, /* create location configuration */ + NULL /* merge location configuration */ }; ngx_module_t ngx_http_pow = { @@ -34,13 +43,242 @@ ngx_module_t ngx_http_pow = { NGX_MODULE_V1_PADDING }; +#define NGX_HTTP_POW_CHALLENGE_SERIALIZED_LENGTH 256 + +typedef struct __attribute__((packed)) ngx_http_pow_challenge_s { + uint32_t version; + uint32_t created; + uint32_t ttl; + uint32_t difficulty; + u_char random[NGX_HTTP_POW_CHALLENGE_RANDOM_LENGTH]; + u_char digest[EVP_MAX_MD_SIZE]; +} ngx_http_pow_challenge_t; + +static ngx_http_pow_challenge_t +*ngx_http_pow_create_challenge(ngx_pool_t *pool) +{ + ngx_http_pow_challenge_t *challenge; + + challenge = ngx_palloc(pool, sizeof(*challenge)); + + challenge->version = 1; + challenge->created = ngx_time(); + challenge->ttl = 30; + + RAND_bytes(challenge->random, NGX_HTTP_POW_CHALLENGE_RANDOM_LENGTH); + + HMAC(EVP_sha256(), NGX_HTTP_POW_HMAC_KEY, sizeof(NGX_HTTP_POW_HMAC_KEY), + (u_char *)challenge, sizeof(*challenge) - EVP_MAX_MD_SIZE, + challenge->digest, NULL); + + return challenge; +} + +static ngx_str_t +*ngx_http_pow_challenge_marshal(ngx_http_pow_challenge_t *challenge, + ngx_pool_t *pool) +{ + ngx_str_t challenge_string; + ngx_str_t *marshalled; + size_t encoded_length; + u_char *buf; + + challenge_string.data = (u_char *)challenge; + challenge_string.len = sizeof(*challenge); + + encoded_length = ngx_base64_encoded_length(sizeof(*challenge)); + buf = ngx_palloc(pool, encoded_length); + marshalled = ngx_palloc(pool, sizeof(ngx_str_t)); + + marshalled->len = encoded_length; + marshalled->data = buf; + + ngx_encode_base64(marshalled, &challenge_string); + + return marshalled; +} + +static ngx_http_pow_challenge_t NGX_HTTP_POW_UNUSED +*ngx_http_pow_challenge_unmarshal(ngx_str_t *marshalled, ngx_pool_t *pool) +{ + ngx_http_pow_challenge_t *challenge; + ngx_str_t *unmarshalled; + + challenge = ngx_palloc(pool, sizeof(*challenge)); + unmarshalled = ngx_palloc(pool, sizeof(ngx_str_t)); + + unmarshalled->data = (u_char *)challenge; + unmarshalled->len = sizeof(*challenge); + + ngx_decode_base64(unmarshalled, marshalled); + + return challenge; +} + +static void +ngx_http_pow_require_pow(ngx_http_request_t *r) +{ + ngx_http_pow_challenge_t *challenge; + ngx_table_elt_t *header; + ngx_str_t *marshalled; + + challenge = ngx_http_pow_create_challenge(r->pool); + marshalled = ngx_http_pow_challenge_marshal(challenge, r->pool); + header = ngx_list_push(&r->headers_out.headers); + + ngx_str_set(&header->key, NGX_HTTP_POW_CHALLENGE_HEADER_KEY); + header->value = *marshalled; + header->hash = 1; + + r->headers_out.status = NGX_HTTP_UNAUTHORIZED; + r->headers_out.content_length = 0; + + ngx_http_send_header(r); + ngx_http_finalize_request(r, NGX_HTTP_UNAUTHORIZED); +} + +typedef struct __attribute__((packed)) ngx_http_pow_s { + ngx_http_pow_challenge_t challenge; + uint32_t nonce; + u_char hash[EVP_MAX_MD_SIZE]; +} ngx_http_pow_t; + +static ngx_str_t NGX_HTTP_POW_UNUSED +*ngx_http_pow_marshal(ngx_http_pow_t *pow, + ngx_pool_t *pool) +{ + ngx_str_t pow_string; + ngx_str_t *marshalled; + size_t encoded_length; + u_char *buf; + + pow_string.data = (u_char *)pow; + pow_string.len = sizeof(*pow); + + encoded_length = ngx_base64_encoded_length(sizeof(*pow)); + buf = ngx_palloc(pool, encoded_length); + marshalled = ngx_palloc(pool, sizeof(ngx_str_t)); + + marshalled->len = encoded_length; + marshalled->data = buf; + + ngx_encode_base64(marshalled, &pow_string); + + return marshalled; +} + +static ngx_http_pow_t +*ngx_http_pow_unmarshal(ngx_pool_t *pool, ngx_str_t *marshalled) +{ + ngx_http_pow_t *pow; + ngx_str_t *unmarshalled; + + pow = ngx_palloc(pool, sizeof(*pow)); + unmarshalled = ngx_palloc(pool, sizeof(ngx_str_t)); + + unmarshalled->data = (u_char *)pow; + unmarshalled->len = sizeof(*pow); + + ngx_decode_base64(unmarshalled, marshalled); + + return pow; +} + +static bool +ngx_http_pow_check_difficulty(ngx_http_pow_t *pow, uint32_t difficulty) +{ + size_t i; + size_t zero_bytes = difficulty / 8; + size_t remaining_bits = difficulty % 8; + + static u_char lut[] = { + 0b11111111, 0b01111111, 0b00111111, 0b00011111, + 0b00001111, 0b00000111, 0b00000011, 0b00000001 + }; + + for (i = 0; i < zero_bytes; i++) { + if (pow->hash[i] > 0) { + return false; + } + } + + if (pow->hash[zero_bytes + 1] ^ lut[remaining_bits]) { + return false; + } + + return true; +} + +static bool +ngx_http_pow_check_digest(void *data, size_t len, u_char *digest) +{ + u_char d[EVP_MAX_MD_SIZE]; + + HMAC(EVP_sha256(), NGX_HTTP_POW_HMAC_KEY, sizeof(NGX_HTTP_POW_HMAC_KEY), + data, len, d, NULL); + + /* CRYPTO_memcmp returns 0 if compared values are equal, else != 0 */ + if (CRYPTO_memcmp(digest, d, EVP_MAX_MD_SIZE)) { + return false; + } + + return true; +} + +static bool +ngx_http_pow_check_pow(ngx_http_request_t *r) +{ + ngx_http_pow_challenge_t *challenge; + ngx_http_pow_t *pow; + ngx_str_t pow_cookie_value; + + ngx_str_t pow_cookie_name = ngx_string(NGX_HTTP_POW_CHALLENGE_COOKIE_NAME); + + /* No PoW cookie is present */ + if (!ngx_http_parse_multi_header_lines(r, r->headers_in.cookie, + &pow_cookie_name, &pow_cookie_value)) { + return false; + } + + pow = ngx_http_pow_unmarshal(r->pool, &pow_cookie_value); + challenge = &pow->challenge; + + /* Check digest matches */ + if (!ngx_http_pow_check_digest(challenge, + sizeof(*challenge) - sizeof(challenge->digest), + challenge->digest)) { + return false; + } + + /* Check pow is still valid */ + if (challenge->created + challenge->ttl > ngx_time()) { + return false; + } + + /* Check pow difficulty */ + ngx_http_pow_check_difficulty(pow, challenge->difficulty); + + /* Check pow is correct */ + if (!ngx_http_pow_check_digest(pow, + sizeof(*pow) - sizeof(pow->hash), + pow->hash)) { + return false; + } + + return true; +} + static ngx_int_t ngx_http_pow_handler(ngx_http_request_t *r) { - ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, - "ngx_http_pow_handler was invoked"); + /* PoW is either not present or presetn, but incorrect, require PoW */ + if (!ngx_http_pow_check_pow(r)) { + ngx_http_pow_require_pow(r); + return NGX_DONE; + } - return NGX_DECLINED; + /* PoW is present and correct, resume processing */ + return NGX_DECLINED; } @@ -59,8 +297,5 @@ ngx_http_pow_init(ngx_conf_t *cf) *h = ngx_http_pow_handler; - ngx_log_debug0(NGX_LOG_DEBUG_HTTP, cf->log, 0, - "ngx_http_pow_init was invoked"); - return NGX_OK; }