diff --git a/README.md b/README.md index 890222f..c3f8ad8 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ Table of Contents * [Status](#status) * [Synopsis](#synopsis) * [Description](#description) +* [Token Format Compatibility](#token-format-compatibility) * [Directives](#directives) * [encrypted_session_key](#encrypted_session_key) * [encrypted_session_iv](#encrypted_session_iv) @@ -41,9 +42,7 @@ Synopsis # key must be of 32 bytes long encrypted_session_key "abcdefghijklmnopqrstuvwxyz123456"; -# iv must not be longer than 16 bytes -# default: "deadbeefdeadbeef" (w/o quotes) -encrypted_session_iv "1234567812345678"; +# a fresh random IV is generated for every token # default: 1d (1 day) encrypted_session_expires 3600; # in sec @@ -74,7 +73,10 @@ Description =========== This module provides encryption and decryption support for -nginx variables based on AES-256 with Mac. +nginx variables using AES-256-CBC and HMAC-SHA256. Each encryption uses a +fresh cryptographically random 16-byte IV. The version, IV and ciphertext +are authenticated before decryption with a separate MAC key derived from +the configured encryption key. This module is usually used with the [ngx_set_misc module](http://github.com/agentzh/set-misc-nginx-module) and the standard rewrite module's directives. @@ -105,6 +107,22 @@ local raw_text = ndk.set_var.set_decrypt_session(encrypted_text) [Back to TOC](#table-of-contents) +Token Format Compatibility +========================== + +Tokens contain a one-byte version (`1`), a 16-byte IV, AES-256-CBC ciphertext, +and a 32-byte HMAC-SHA256 tag over all preceding bytes. The encrypted data +contains the original value followed by its eight-byte big-endian expiration +timestamp. The MAC key is derived as +`HMAC-SHA256(encrypted_session_key, "encrypted-session MAC v1")`. + +The configured key must be random and secret. Legacy public-MD5, fixed-IV +tokens are rejected; existing users must obtain new sessions after upgrading. +All instances that exchange tokens must use the new format before issuing +new sessions. Repeated encryptions of the same value produce different tokens. + +[Back to TOC](#table-of-contents) + Directives ========== @@ -134,7 +152,9 @@ encrypted_session_iv **context:** *http, server, server if, location, location if* -Sets the initial vector used for the cipher (must be *no longer* than 16 bytes). +Accepted for configuration compatibility (must be *no longer* than 16 bytes). +This directive no longer selects the encryption IV. Every token carries its +own randomly generated IV, and decryption uses that authenticated IV. For example, @@ -365,4 +385,3 @@ See Also * [ngx_set_misc module](http://github.com/agentzh/set-misc-nginx-module) [Back to TOC](#table-of-contents) - diff --git a/src/ngx_http_encrypted_session_cipher.c b/src/ngx_http_encrypted_session_cipher.c index 460dda7..17cfe13 100644 --- a/src/ngx_http_encrypted_session_cipher.c +++ b/src/ngx_http_encrypted_session_cipher.c @@ -12,7 +12,9 @@ #include "ngx_http_encrypted_session_cipher.h" #include -#include +#include +#include +#include #include #include @@ -21,22 +23,60 @@ static uint64_t ngx_http_encrypted_session_ntohll(uint64_t n); static uint64_t ngx_http_encrypted_session_htonll(uint64_t n); +/* + * Version || unpredictable IV || AES-CBC ciphertext || HMAC-SHA256. + * Authenticate the version and IV along with the ciphertext before decrypting. + * The configured IV is retained in the API for configuration compatibility. + */ +#define SESSION_VERSION 1 +#define SESSION_IV_SIZE 16 +#define SESSION_BLOCK_SIZE 16 +#define SESSION_HEADER_SIZE (1 + SESSION_IV_SIZE) +#define SESSION_MAC_SIZE ngx_http_encrypted_session_mac_length + + +ngx_int_t +ngx_http_encrypted_session_init_key(ngx_http_encrypted_session_key_t *key, + const u_char *data, size_t len) +{ + static const u_char domain[] = "encrypted-session MAC v1"; + unsigned int mac_key_len; + + if (len != ngx_http_encrypted_session_key_length) { + return NGX_ERROR; + } + + if (HMAC(EVP_sha256(), data, (int) len, domain, sizeof(domain) - 1, + key->mac_key, &mac_key_len) == NULL + || mac_key_len != SESSION_MAC_SIZE) + { + OPENSSL_cleanse(key, sizeof(*key)); + return NGX_ERROR; + } + + ngx_memcpy(key->key, data, len); + return NGX_OK; +} + + ngx_int_t ngx_http_encrypted_session_aes_mac_encrypt( ngx_http_encrypted_session_main_conf_t *emcf, ngx_pool_t *pool, - ngx_log_t *log, const u_char *iv, size_t iv_len, const u_char *key, - size_t key_len, const u_char *in, size_t in_len, ngx_uint_t expires, + ngx_log_t *log, const u_char *iv, size_t iv_len, + const ngx_http_encrypted_session_key_t *key, + const u_char *in, size_t in_len, ngx_uint_t expires, u_char **dst, size_t *dst_len) { - const EVP_CIPHER *cipher; - u_char *p, *data; - int ret; - size_t block_size, buf_size, data_size; - int len; - uint64_t expires_time; - time_t now; - - if (key_len != ngx_http_encrypted_session_key_length) { + u_char *token, *data; + size_t data_size, capacity, encrypted; + int len, tail; + unsigned int mac_len; + uint64_t expires_time; + time_t now; + + if (key == NULL + || in_len > INT_MAX - sizeof(expires_time) - SESSION_BLOCK_SIZE) + { return NGX_ERROR; } @@ -50,208 +90,134 @@ ngx_http_encrypted_session_aes_mac_encrypt( } } - cipher = EVP_aes_256_cbc(); - - block_size = EVP_CIPHER_block_size(cipher); - data_size = in_len + sizeof(expires_time); - - buf_size = MD5_DIGEST_LENGTH /* for the digest */ - + (data_size + block_size - 1) /* for EVP_EncryptUpdate */ - + block_size; /* for EVP_EncryptFinal */ - - p = ngx_palloc(pool, buf_size + data_size); - if (p == NULL) { - goto evp_error; + capacity = SESSION_HEADER_SIZE + data_size + SESSION_BLOCK_SIZE + + SESSION_MAC_SIZE; + token = ngx_palloc(pool, capacity); + data = ngx_palloc(pool, data_size); + if (token == NULL || data == NULL) { + return NGX_ERROR; } - *dst = p; - - data = p + buf_size; + token[0] = SESSION_VERSION; + if (RAND_bytes(token + 1, SESSION_IV_SIZE) != 1) { + ngx_log_error(NGX_LOG_ERR, log, 0, + "encrypted_session: failed to generate session IV"); - ngx_memcpy(data, in, in_len); + return NGX_ERROR; + } - if (expires == 0) { - expires_time = 0; - } else { + expires_time = 0; + if (expires != 0) { now = time(NULL); if (now == -1) { - goto evp_error; + return NGX_ERROR; } expires_time = (uint64_t) now + (uint64_t) expires; } - dd("expires before encryption: %lld", (long long) expires_time); - expires_time = ngx_http_encrypted_session_htonll(expires_time); - - ngx_memcpy(data + in_len, (u_char *) &expires_time, sizeof(expires_time)); - - MD5(data, data_size, p); - - p += MD5_DIGEST_LENGTH; - - ret = EVP_EncryptInit(emcf->session_ctx, cipher, key, iv); - if (!ret) { - goto evp_error; - } - - /* encrypt the raw input data */ - - ret = EVP_EncryptUpdate(emcf->session_ctx, p, &len, data, data_size); - if (!ret) { - goto evp_error; - } - - p += len; - - ret = EVP_EncryptFinal_ex(emcf->session_ctx, p, &len); - - emcf->reset_cipher_ctx(emcf->session_ctx); - - if (!ret) { + ngx_memcpy(data, in, in_len); + ngx_memcpy(data + in_len, &expires_time, sizeof(expires_time)); + + if (EVP_EncryptInit_ex(emcf->session_ctx, EVP_aes_256_cbc(), NULL, + key->key, token + 1) != 1 + || EVP_EncryptUpdate(emcf->session_ctx, token + SESSION_HEADER_SIZE, + &len, data, (int) data_size) != 1 + || EVP_EncryptFinal_ex(emcf->session_ctx, + token + SESSION_HEADER_SIZE + len, &tail) != 1) + { + emcf->reset_cipher_ctx(emcf->session_ctx); return NGX_ERROR; } - p += len; - - *dst_len = p - *dst; - - if (*dst_len > buf_size) { - ngx_log_error(NGX_LOG_ERR, log, 0, - "encrypted_session: aes_mac_encrypt: buffer error"); - + emcf->reset_cipher_ctx(emcf->session_ctx); + encrypted = SESSION_HEADER_SIZE + len + tail; + if (HMAC(EVP_sha256(), key->mac_key, SESSION_MAC_SIZE, token, encrypted, + token + encrypted, &mac_len) == NULL + || mac_len != SESSION_MAC_SIZE) + { return NGX_ERROR; } + *dst = token; + *dst_len = encrypted + SESSION_MAC_SIZE; return NGX_OK; - -evp_error: - - emcf->reset_cipher_ctx(emcf->session_ctx); - - return NGX_ERROR; } ngx_int_t ngx_http_encrypted_session_aes_mac_decrypt( ngx_http_encrypted_session_main_conf_t *emcf, ngx_pool_t *pool, - ngx_log_t *log, const u_char *iv, size_t iv_len, const u_char *key, - size_t key_len, const u_char *in, size_t in_len, u_char **dst, + ngx_log_t *log, const u_char *iv, size_t iv_len, + const ngx_http_encrypted_session_key_t *key, + const u_char *in, size_t in_len, u_char **dst, size_t *dst_len) { - const EVP_CIPHER *cipher; - int ret; - size_t block_size, buf_size; - int len; - u_char *p; - const u_char *digest; - uint64_t expires_time; - time_t now; - - u_char new_digest[MD5_DIGEST_LENGTH]; - - if (key_len != ngx_http_encrypted_session_key_length - || in_len < MD5_DIGEST_LENGTH) + u_char mac[SESSION_MAC_SIZE], *data; + size_t authenticated, encrypted; + int len, tail; + unsigned int mac_len; + uint64_t expires_time; + time_t now; + + if (key == NULL + || in_len < SESSION_HEADER_SIZE + SESSION_BLOCK_SIZE + SESSION_MAC_SIZE + || in[0] != SESSION_VERSION) { return NGX_ERROR; } - digest = in; + authenticated = in_len - SESSION_MAC_SIZE; + encrypted = authenticated - SESSION_HEADER_SIZE; + if (encrypted > INT_MAX || encrypted % SESSION_BLOCK_SIZE != 0 + || HMAC(EVP_sha256(), key->mac_key, SESSION_MAC_SIZE, in, authenticated, + mac, &mac_len) == NULL + || mac_len != SESSION_MAC_SIZE + || CRYPTO_memcmp(mac, in + authenticated, SESSION_MAC_SIZE) != 0) + { + return NGX_ERROR; + } if (emcf->session_ctx == NULL) { emcf->session_ctx = EVP_CIPHER_CTX_new(); if (emcf->session_ctx == NULL) { ngx_log_error(NGX_LOG_ERR, log, 0, - "encrypted_session: aes_mac_encrypt: no memory"); + "encrypted_session: aes_mac_decrypt: no memory"); return NGX_ERROR; } } - cipher = EVP_aes_256_cbc(); - - ret = EVP_DecryptInit(emcf->session_ctx, cipher, key, iv); - if (!ret) { - goto evp_error; - } - - block_size = EVP_CIPHER_block_size(cipher); - - buf_size = in_len + block_size /* for EVP_DecryptUpdate */ - + block_size; /* for EVP_DecryptFinal_ex */ - - p = ngx_palloc(pool, buf_size); - if (p == NULL) { - goto evp_error; - } - - *dst = p; - - ret = EVP_DecryptUpdate(emcf->session_ctx, p, &len, in + MD5_DIGEST_LENGTH, - in_len - MD5_DIGEST_LENGTH); - - if (!ret) { - dd("decrypt update failed"); - goto evp_error; - } - - p += len; - - ret = EVP_DecryptFinal_ex(emcf->session_ctx, p, &len); - - emcf->reset_cipher_ctx(emcf->session_ctx); - - if (!ret) { - ngx_log_debug0(NGX_LOG_DEBUG_HTTP, log, 0, - "failed to decrypt session: bad AES-256 digest"); - - return NGX_ERROR; - } - - p += len; - - *dst_len = p - *dst; - - if (*dst_len > buf_size) { - ngx_log_error(NGX_LOG_ERR, log, 0, - "encrypted_session: aes_mac_decrypt: buffer error"); - + data = ngx_palloc(pool, encrypted); + if (data == NULL) { return NGX_ERROR; } - if (*dst_len < sizeof(expires_time)) { + if (EVP_DecryptInit_ex(emcf->session_ctx, EVP_aes_256_cbc(), NULL, + key->key, in + 1) != 1 + || EVP_DecryptUpdate(emcf->session_ctx, data, &len, + in + SESSION_HEADER_SIZE, (int) encrypted) != 1 + || EVP_DecryptFinal_ex(emcf->session_ctx, data + len, &tail) != 1) + { + emcf->reset_cipher_ctx(emcf->session_ctx); return NGX_ERROR; } - MD5(*dst, *dst_len, new_digest); - - if (CRYPTO_memcmp(digest, new_digest, MD5_DIGEST_LENGTH) != 0) { - ngx_log_debug0(NGX_LOG_DEBUG_HTTP, log, 0, - "failed to decrypt session: MD5 checksum mismatch"); - + emcf->reset_cipher_ctx(emcf->session_ctx); + if ((size_t) (len + tail) < sizeof(expires_time)) { return NGX_ERROR; } - *dst_len -= sizeof(expires_time); - - dd("dst len: %d", (int) *dst_len); - dd("dst: %.*s", (int) *dst_len, *dst); - - p -= sizeof(expires_time); - - ngx_memcpy(&expires_time, p, sizeof(expires_time)); + *dst_len = len + tail - sizeof(expires_time); + ngx_memcpy(&expires_time, data + *dst_len, sizeof(expires_time)); expires_time = ngx_http_encrypted_session_ntohll(expires_time); - now = time(NULL); if (now == -1) { return NGX_ERROR; } - dd("expires after decryption: %lld", (long long) expires_time); - if (expires_time && expires_time <= (uint64_t) now) { ngx_log_debug2(NGX_LOG_DEBUG_HTTP, log, 0, "encrypted_session: session expired: %uL <= %T", @@ -259,15 +225,8 @@ ngx_http_encrypted_session_aes_mac_decrypt( return NGX_ERROR; } - dd("decrypted successfully"); - + *dst = data; return NGX_OK; - -evp_error: - - emcf->reset_cipher_ctx(emcf->session_ctx); - - return NGX_ERROR; } diff --git a/src/ngx_http_encrypted_session_cipher.h b/src/ngx_http_encrypted_session_cipher.h index 2ac4718..d451c51 100644 --- a/src/ngx_http_encrypted_session_cipher.h +++ b/src/ngx_http_encrypted_session_cipher.h @@ -18,20 +18,33 @@ typedef struct { enum { ngx_http_encrypted_session_key_length = 256 / 8, + ngx_http_encrypted_session_mac_length = 256 / 8, ngx_http_encrypted_session_iv_length = EVP_MAX_IV_LENGTH }; +typedef struct { + u_char key[ngx_http_encrypted_session_key_length]; + u_char mac_key[ngx_http_encrypted_session_mac_length]; +} ngx_http_encrypted_session_key_t; + + +ngx_int_t ngx_http_encrypted_session_init_key( + ngx_http_encrypted_session_key_t *key, const u_char *data, size_t len); + + ngx_int_t ngx_http_encrypted_session_aes_mac_encrypt( ngx_http_encrypted_session_main_conf_t *emcf, ngx_pool_t *pool, - ngx_log_t *log, const u_char *iv, size_t iv_len, const u_char *key, - size_t key_len, const u_char *in, size_t in_len, + ngx_log_t *log, const u_char *iv, size_t iv_len, + const ngx_http_encrypted_session_key_t *key, + const u_char *in, size_t in_len, ngx_uint_t expires, u_char **dst, size_t *dst_len); ngx_int_t ngx_http_encrypted_session_aes_mac_decrypt( ngx_http_encrypted_session_main_conf_t *emcf, ngx_pool_t *pool, - ngx_log_t *log, const u_char *iv, size_t iv_len, const u_char *key, - size_t key_len, const u_char *in, size_t in_len, u_char **dst, + ngx_log_t *log, const u_char *iv, size_t iv_len, + const ngx_http_encrypted_session_key_t *key, + const u_char *in, size_t in_len, u_char **dst, size_t *dst_len); diff --git a/src/ngx_http_encrypted_session_module.c b/src/ngx_http_encrypted_session_module.c index a2206be..554d49d 100644 --- a/src/ngx_http_encrypted_session_module.c +++ b/src/ngx_http_encrypted_session_module.c @@ -10,6 +10,7 @@ #include "ddebug.h" #include +#include #include "ngx_http_encrypted_session_cipher.h" #define ngx_http_encrypted_session_default_iv (u_char *) "deadbeefdeadbeef" @@ -18,9 +19,9 @@ typedef struct { - u_char *key; - u_char *iv; - time_t expires; + ngx_http_encrypted_session_key_t *key; + u_char *iv; + time_t expires; } ngx_http_encrypted_session_conf_t; @@ -32,6 +33,7 @@ static ngx_int_t ngx_http_set_decode_encrypted_session(ngx_http_request_t *r, ngx_str_t *res, ngx_http_variable_value_t *v); static void ngx_http_encrypted_session_free_cipher_ctx(void *data); +static void ngx_http_encrypted_session_free_key(void *data); static char *ngx_http_encrypted_session_key(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); @@ -178,8 +180,7 @@ ngx_http_set_encode_encrypted_session(ngx_http_request_t *r, rc = ngx_http_encrypted_session_aes_mac_encrypt(emcf, r->pool, r->connection->log, conf->iv, ngx_http_encrypted_session_iv_length, - conf->key, ngx_http_encrypted_session_key_length, - v->data, v->len, (ngx_uint_t) conf->expires, &dst, &len); + conf->key, v->data, v->len, (ngx_uint_t) conf->expires, &dst, &len); if (rc != NGX_OK) { dst = NULL; @@ -220,8 +221,7 @@ ngx_http_set_decode_encrypted_session(ngx_http_request_t *r, rc = ngx_http_encrypted_session_aes_mac_decrypt(emcf, r->pool, r->connection->log, conf->iv, ngx_http_encrypted_session_iv_length, - conf->key, ngx_http_encrypted_session_key_length, - v->data, v->len, &dst, &len); + conf->key, v->data, v->len, &dst, &len); if (rc != NGX_OK) { dst = NULL; @@ -238,7 +238,9 @@ ngx_http_set_decode_encrypted_session(ngx_http_request_t *r, static char * ngx_http_encrypted_session_key(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) { - ngx_str_t *value; + ngx_str_t *value; + ngx_pool_cleanup_t *cln; + ngx_http_encrypted_session_key_t *key; ngx_http_encrypted_session_conf_t *llcf = conf; @@ -257,7 +259,24 @@ ngx_http_encrypted_session_key(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) return NGX_CONF_ERROR; } - llcf->key = value[1].data; + cln = ngx_pool_cleanup_add(cf->pool, sizeof(*key)); + if (cln == NULL) { + return NGX_CONF_ERROR; + } + + key = cln->data; + cln->handler = ngx_http_encrypted_session_free_key; + + if (ngx_http_encrypted_session_init_key(key, value[1].data, value[1].len) + != NGX_OK) + { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "encrypted_session_key: failed to derive MAC key"); + + return NGX_CONF_ERROR; + } + + llcf->key = key; return NGX_CONF_OK; } @@ -327,6 +346,13 @@ ngx_http_encrypted_session_expires(ngx_conf_t *cf, ngx_command_t *cmd, } +static void +ngx_http_encrypted_session_free_key(void *data) +{ + OPENSSL_cleanse(data, sizeof(ngx_http_encrypted_session_key_t)); +} + + static void ngx_http_encrypted_session_free_cipher_ctx(void *data) { diff --git a/t/bundle-regressions.t b/t/bundle-regressions.t new file mode 100644 index 0000000..e3b0db4 --- /dev/null +++ b/t/bundle-regressions.t @@ -0,0 +1,41 @@ +use Test::Nginx::Socket::Lua 'no_plan'; +use Cwd qw(cwd); + +my $pwd = cwd(); +our $HttpConfig = qq{ + encrypted_session_key "abcdefghijklmnopqrstuvwxyz123456"; + encrypted_session_expires 0; + lua_package_path "$pwd/lib/?.lua;$pwd/src/?.lua;;"; + lua_package_cpath "$pwd/?.so;;"; +}; + +no_long_string(); +run_tests(); + +__DATA__ + +=== TEST 1: randomized authenticated tokens and legacy rejection +--- http_config eval: $::HttpConfig +--- config + location = /t { + content_by_lua_block { + local encrypt = ndk.set_var.set_encrypt_session + local decrypt = ndk.set_var.set_decrypt_session + local a, b = encrypt("same value"), encrypt("same value") + assert(a ~= b and #a >= 65) + assert(decrypt(a) == "same value") + for _, i in ipairs({1, 2, 18, #a}) do + local byte = string.byte(a, i) + local altered = a:sub(1, i-1) .. string.char((byte+1)%256) .. a:sub(i+1) + assert(decrypt(altered) == "") + end + assert(decrypt(string.rep("x",32)) == "") + ngx.say("ok") + } + } +--- request +GET /t +--- response_body +ok +--- no_error_log +[error] diff --git a/t/crypto.t b/t/crypto.t new file mode 100644 index 0000000..d421212 --- /dev/null +++ b/t/crypto.t @@ -0,0 +1,329 @@ +# vi:filetype=perl + +use Test::Nginx::Socket::Lua; + +repeat_each(1); +plan tests => repeat_each() * blocks() * 3; +no_long_string(); +run_tests(); + +__DATA__ + +=== TEST 1: independent token vector and expiration +--- http_config + encrypted_session_key "abcdefghijklmnopqrstuvwxyz123456"; + encrypted_session_expires 0; +--- config + location = /t { + content_by_lua_block { + -- Generated independently using openssl enc and Python hmac. + local valid = ngx.decode_base64("AQABAgMEBQYHCAkKCwwNDg8wHLhcqphxAGw+4b6eKNQ0XXjKwqCrjv/RT/DC2aaEzt+1JFWadoSoLuW/cnlBl3SZmNKN5UjPPYmLm1l+lcnc") + local expired = ngx.decode_base64("AQABAgMEBQYHCAkKCwwNDg+au6AHza8Q2hKgHeOkfQN49/7CeBbZ+J1/2GFqPw/Us/neLhV086Y7OiiufhXhVnlGRHqb7+WkCeZ8Xu76z2zA") + local decrypt = ndk.set_var.set_decrypt_session + ngx.say(decrypt(valid) == "known vector\0value") + ngx.say(decrypt(expired) == "") + ngx.say(decrypt(valid) == "known vector\0value") + } + } +--- request +GET /t +--- error_code: 200 +--- response_body +true +true +true +--- no_error_log +[error] + + + +=== TEST 2: IV and first ciphertext block vary for identical plaintext prefixes +--- http_config + encrypted_session_key "abcdefghijklmnopqrstuvwxyz123456"; + encrypted_session_iv "fixed iv"; + encrypted_session_expires 0; +--- config + location = /t { + content_by_lua_block { + local encrypt = ndk.set_var.set_encrypt_session + local decrypt = ndk.set_var.set_decrypt_session + local prefix = "0123456789abcdef" + local a = encrypt(prefix .. "one") + local b = encrypt(prefix .. "two") + local c = encrypt(prefix .. "one") + ngx.say(#a == 81 and #b == 81 and #c == 81) + ngx.say(a:byte(1) == 1 and b:byte(1) == 1) + ngx.say(a:sub(2, 17) ~= b:sub(2, 17)) + ngx.say(a:sub(18, 33) ~= b:sub(18, 33)) + ngx.say(a:sub(2, 17) ~= c:sub(2, 17)) + ngx.say(a:sub(18, 33) ~= c:sub(18, 33)) + ngx.say(decrypt(a) == prefix .. "one") + ngx.say(decrypt(b) == prefix .. "two") + } + } +--- request +GET /t +--- error_code: 200 +--- response_body +true +true +true +true +true +true +true +true +--- no_error_log +[error] + + + +=== TEST 3: every token byte is authenticated and malformed tokens are rejected +--- http_config + encrypted_session_key "abcdefghijklmnopqrstuvwxyz123456"; + encrypted_session_expires 0; +--- config + location = /t { + content_by_lua_block { + local encrypt = ndk.set_var.set_encrypt_session + local decrypt = ndk.set_var.set_decrypt_session + local token = encrypt("authenticated value") + local rejected = 0 + for i = 1, #token do + local altered = token:sub(1, i - 1) + .. string.char((token:byte(i) + 1) % 256) + .. token:sub(i + 1) + if decrypt(altered) == "" then + rejected = rejected + 1 + end + end + + ngx.say(rejected == #token) + rejected = 0 + for i = 0, #token - 1 do + if decrypt(token:sub(1, i)) == "" then + rejected = rejected + 1 + end + end + + ngx.say(rejected == #token) + ngx.say(decrypt(token .. "x") == "") + ngx.say(decrypt(token) == "authenticated value") + } + } +--- request +GET /t +--- error_code: 200 +--- response_body +true +true +true +true +--- no_error_log +[error] + + + +=== TEST 4: legacy token from the old sanity test is rejected +--- http_config + encrypted_session_key "abcdefghijklmnopqrstuvwxyz123456"; + encrypted_session_expires 0; +--- config + location = /t { + content_by_lua_block { + local old = ndk.set_var.set_decode_base32( + "ktrp3n437q42laejppc9d4bg0jpv0ejie106ooo65od9lf5huhs0====") + ngx.say(#old == 32) + ngx.say(ndk.set_var.set_decrypt_session(old) == "") + local token = ndk.set_var.set_encrypt_session("abc") + ngx.say(ndk.set_var.set_decrypt_session(token) == "abc") + } + } +--- request +GET /t +--- error_code: 200 +--- response_body +true +true +true +--- no_error_log +[error] + + + +=== TEST 5: binary values and block boundaries round trip +--- http_config + encrypted_session_key "abcdefghijklmnopqrstuvwxyz123456"; + encrypted_session_expires 0; +--- config + location = /t { + content_by_lua_block { + local encrypt = ndk.set_var.set_encrypt_session + local decrypt = ndk.set_var.set_decrypt_session + local passed = 0 + for _, len in ipairs({0, 1, 7, 8, 9, 15, 16, 17, 23, 24, 25, 32, 1024}) do + local value = string.rep("\0", len) + local token = encrypt(value) + local expected = 49 + (math.floor((len + 8) / 16) + 1) * 16 + if #token == expected and decrypt(token) == value then + passed = passed + 1 + end + end + + ngx.say(passed) + } + } +--- request +GET /t +--- error_code: 200 +--- response_body +13 +--- no_error_log +[error] + + + +=== TEST 6: authentication rejects a different key while configured IV is ignored +--- http_config + encrypted_session_key "abcdefghijklmnopqrstuvwxyz123456"; + encrypted_session_expires 0; +--- config + location = /other-key { + encrypted_session_key "123456abcdefghijklmnopqrstuvwxyz"; + content_by_lua_block { + local token = ngx.decode_base64(ngx.req.get_uri_args().token) + ngx.print(ndk.set_var.set_decrypt_session(token)) + } + } + + location = /other-iv { + encrypted_session_iv "another iv"; + content_by_lua_block { + local token = ngx.decode_base64(ngx.req.get_uri_args().token) + ngx.print(ndk.set_var.set_decrypt_session(token)) + } + } + + location = /t { + content_by_lua_block { + local token = ndk.set_var.set_encrypt_session("same key") + local options = { args = { token = ngx.encode_base64(token) } } + local other_key = ngx.location.capture("/other-key", options) + local other_iv = ngx.location.capture("/other-iv", options) + ngx.say(other_key.status, " ", other_key.body == "") + ngx.say(other_iv.status, " ", other_iv.body) + } + } +--- request +GET /t +--- error_code: 200 +--- response_body +200 true +200 same key +--- no_error_log +[error] + + + +=== TEST 7: authenticated malformed plaintext is rejected and cipher context recovers +--- http_config + encrypted_session_key "abcdefghijklmnopqrstuvwxyz123456"; + encrypted_session_expires 0; +--- config + location = /t { + content_by_lua_block { + -- Independent vectors with valid HMACs and invalid CBC payloads. + local bad_padding = ngx.decode_base64("AQABAgMEBQYHCAkKCwwNDg8AAAAAAAAAAAAAAAAAAAAA8cnoax+tYKZChe19vAwgTkzfV8ZZsKucaux6G8LyA40=") + local no_expiry = ngx.decode_base64("AQABAgMEBQYHCAkKCwwNDg8/3ncdB5RAYVYEWd2Dl6uJdvZc/qQ3W4n9PbjqcBRucgdLVC5fVZ1SP0aiT/3VtxQ=") + local decrypt = ndk.set_var.set_decrypt_session + ngx.say(decrypt(bad_padding) == "") + ngx.say(decrypt(no_expiry) == "") + local token = ndk.set_var.set_encrypt_session("after invalid padding") + ngx.say(decrypt(token) == "after invalid padding") + } + } +--- request +GET /t +--- error_code: 200 +--- response_body +true +true +true +--- no_error_log +[error] + + + +=== TEST 8: configured keys stay isolated and inherited keys survive alternating use +--- http_config + encrypted_session_key "abcdefghijklmnopqrstuvwxyz123456"; + encrypted_session_expires 0; +--- config + location = /key-a { + content_by_lua_block { + local token = ngx.req.get_uri_args().token + if token then + ngx.print(ndk.set_var.set_decrypt_session(ngx.decode_base64(token))) + + else + ngx.print(ngx.encode_base64(ndk.set_var.set_encrypt_session("cached key value"))) + end + } + } + + location /key-b/ { + encrypted_session_key "123456abcdefghijklmnopqrstuvwxyz"; + content_by_lua_block { + local token = ngx.req.get_uri_args().token + if token then + ngx.print(ndk.set_var.set_decrypt_session(ngx.decode_base64(token))) + + else + ngx.print(ngx.encode_base64(ndk.set_var.set_encrypt_session("cached key value"))) + end + } + + location = /key-b/inherited { + content_by_lua_block { + local token = ngx.req.get_uri_args().token + if token then + ngx.print(ndk.set_var.set_decrypt_session(ngx.decode_base64(token))) + + else + ngx.print(ngx.encode_base64(ndk.set_var.set_encrypt_session("cached key value"))) + end + } + } + } + + location = /t { + content_by_lua_block { + local paths = { "/key-a", "/key-b/", "/key-b/inherited" } + local passed = 0 + for round = 1, 3 do + for i, source in ipairs(paths) do + local issued = ngx.location.capture(source) + assert(issued.status == 200 and #issued.body > 0) + for j, target in ipairs(paths) do + local res = ngx.location.capture(target, { + args = { token = issued.body } + }) + local expected = (i == 1) == (j == 1) + and "cached key value" or "" + if res.status == 200 and res.body == expected then + passed = passed + 1 + end + end + end + end + + ngx.say(passed) + } + } +--- request +GET /t +--- error_code: 200 +--- response_body +27 +--- no_error_log +[error] diff --git a/t/random-failure.c b/t/random-failure.c new file mode 100644 index 0000000..7f5350d --- /dev/null +++ b/t/random-failure.c @@ -0,0 +1,27 @@ +/* Test-only RAND_bytes failure injection, loaded with LD_PRELOAD. */ +#define _GNU_SOURCE +#include + + +static int fail_random; + + +void +encrypted_session_test_rand_fail(int enabled) +{ + fail_random = enabled; +} + + +int +RAND_bytes(unsigned char *buf, int len) +{ + int (*real_rand_bytes)(unsigned char *, int); + + if (fail_random) { + return 0; + } + + real_rand_bytes = dlsym(RTLD_NEXT, "RAND_bytes"); + return real_rand_bytes(buf, len); +} diff --git a/t/random-failure.t b/t/random-failure.t new file mode 100644 index 0000000..37f5686 --- /dev/null +++ b/t/random-failure.t @@ -0,0 +1,63 @@ +# vi:filetype=perl + +use File::Temp qw(tempdir); +use Test::More (); + +BEGIN { + Test::More::plan(skip_all => + 'RAND_bytes fault injection requires Linux LD_PRELOAD') + if $^O ne 'linux'; + + my $dir = tempdir('encrypted-session-random-XXXXXX', TMPDIR => 1, + CLEANUP => 1); + my $lib = "$dir/random-failure.so"; + system($ENV{CC} || 'cc', '-shared', '-fPIC', '-Wall', '-Wextra', '-Werror', + 't/random-failure.c', '-o', $lib, '-ldl') == 0 + or Test::More::BAIL_OUT('cannot build RAND_bytes fault injector'); + + # Let Test::Nginx inherit LD_PRELOAD before it initializes its environment. + # Per-test preload assignments otherwise become the Valgrind executable. + $ENV{LD_PRELOAD} = join ' ', $lib, ($ENV{LD_PRELOAD} || ()); +} + +use Test::Nginx::Socket::Lua; + +workers(1); +repeat_each(1); +plan tests => repeat_each() * blocks() * 4; +no_long_string(); +run_tests(); + +__DATA__ + +=== TEST 1: failed random IV generation emits no token and later requests recover +--- http_config + encrypted_session_key "abcdefghijklmnopqrstuvwxyz123456"; + encrypted_session_expires 0; +--- config + location = /t { + content_by_lua_block { + local ffi = require "ffi" + ffi.cdef[[void encrypted_session_test_rand_fail(int enabled);]] + local encrypt = ndk.set_var.set_encrypt_session + local decrypt = ndk.set_var.set_decrypt_session + local before = encrypt("before failure") + ffi.C.encrypted_session_test_rand_fail(1) + ngx.say(encrypt("must not emit a token") == "") + ffi.C.encrypted_session_test_rand_fail(0) + local after = encrypt("after failure") + ngx.say(decrypt(after) == "after failure") + ngx.say(decrypt(before) == "before failure") + } + } +--- request +GET /t +--- error_code: 200 +--- response_body +true +true +true +--- error_log +encrypted_session: failed to generate session IV +--- no_error_log +[alert] diff --git a/t/sanity.t b/t/sanity.t index 18a7f48..f3f064e 100644 --- a/t/sanity.t +++ b/t/sanity.t @@ -36,9 +36,9 @@ __DATA__ } --- request GET /encode ---- response_body -res = ktrp3n437q42laejppc9d4bg0jpv0ejie106ooo65od9lf5huhs0==== -b = abc +--- response_body_like +^res = [0-9a-v=]{104} +b = abc$ --- error_log encrypted_session: expires=0 @@ -64,9 +64,9 @@ encrypted_session: expires=0 } --- request GET /encode ---- response_body -res = ktrp3n437q42laejppc9d4bg0hul4pmqhc8tn2laae40aakqfoh0==== -b = abc +--- response_body_like +^res = [0-9a-v=]{104} +b = abc$ @@ -92,7 +92,7 @@ b = abc --- request GET /encode --- response_body_like -^res = [0-9a-v=]{30,} +^res = [0-9a-v=]{104} b = abc$ --- error_log encrypted_session: expires=3 @@ -114,7 +114,7 @@ encrypted_session: expires=3 --- request GET /encode --- response_body_like -^res = [0-9a-v=]{30,}$ +^res = [0-9a-v=]{104}$ @@ -315,4 +315,3 @@ encrypted_session: expires=86400 X-Foo: [a-z0-9=]+$ --- error_log encrypted_session: expires=1382400 -