From bb3412bc9276fba16cca5bdad70e20a8bcf987d6 Mon Sep 17 00:00:00 2001 From: lijunlong Date: Sun, 25 Oct 2026 18:29:55 +0800 Subject: [PATCH] bugfix: authenticate session tokens and randomize CBC IVs. Replace the public MD5 prefix and fixed CBC IV with a versioned token containing a random IV and an HMAC-SHA256 tag. Authenticate the header and ciphertext before decryption. Derive the MAC key once per configured encryption key and share cached key material with inheriting configurations. Cleanse keys when the configuration pool is released, and rebuild them on reload. Add independent token vectors, tamper and expiry checks, binary boundary coverage, key isolation and inheritance checks, and random-source failure injection. Load the fault injector before Test::Nginx initializes so it also works under Valgrind. Validation: all 89 Test::Nginx assertions pass normally and under Valgrind on nginx 1.31.8 with OpenSSL 3.5.7. Additional probes confirm key derivation is absent from the request path and key rotation works after reload. BREAKING CHANGE: Legacy session tokens are rejected. The configured IV is ignored; all nodes exchanging tokens must upgrade together, and existing sessions must be reissued. --- README.md | 31 ++- src/ngx_http_encrypted_session_cipher.c | 289 +++++++++------------ src/ngx_http_encrypted_session_cipher.h | 21 +- src/ngx_http_encrypted_session_module.c | 44 +++- t/bundle-regressions.t | 41 +++ t/crypto.t | 329 ++++++++++++++++++++++++ t/random-failure.c | 27 ++ t/random-failure.t | 63 +++++ t/sanity.t | 17 +- 9 files changed, 669 insertions(+), 193 deletions(-) create mode 100644 t/bundle-regressions.t create mode 100644 t/crypto.t create mode 100644 t/random-failure.c create mode 100644 t/random-failure.t 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 -