From b756ab468863d81a6a06251364ed63081940afc4 Mon Sep 17 00:00:00 2001 From: Sunny Chan Date: Tue, 17 Jun 2025 08:30:04 +0000 Subject: [PATCH 1/3] add: ssl.get_shared_ssl_ciphers --- lib/ngx/ssl.lua | 396 ++++++++++++++++++++++++++++++++++++++++++++++++ lib/ngx/ssl.md | 47 ++++++ t/ssl.t | 60 ++++++++ 3 files changed, 503 insertions(+) diff --git a/lib/ngx/ssl.lua b/lib/ngx/ssl.lua index b696beaf4..3ce86c223 100644 --- a/lib/ngx/ssl.lua +++ b/lib/ngx/ssl.lua @@ -9,15 +9,23 @@ local ffi = require "ffi" local C = ffi.C local ffi_str = ffi.string local ffi_gc = ffi.gc +local ffi_copy = ffi.copy +local ffi_sizeof = ffi.sizeof +local ffi_typeof = ffi.typeof +local ffi_new = ffi.new local get_request = base.get_request local error = error local tonumber = tonumber +local format = string.format +local concat = table.concat local errmsg = base.get_errmsg_ptr() local get_string_buf = base.get_string_buf local get_size_ptr = base.get_size_ptr local FFI_DECLINED = base.FFI_DECLINED local FFI_OK = base.FFI_OK local subsystem = ngx.config.subsystem +local table_new = require "table.new" +local table_insert = table.insert local ngx_lua_ffi_ssl_set_der_certificate @@ -43,6 +51,7 @@ local ngx_lua_ffi_ssl_client_random local ngx_lua_ffi_ssl_export_keying_material local ngx_lua_ffi_ssl_export_keying_material_early local ngx_lua_ffi_get_req_ssl_pointer +local ngx_lua_ffi_req_shared_ssl_ciphers if subsystem == 'http' then @@ -114,6 +123,14 @@ if subsystem == 'http' then unsigned char *out, size_t out_size, const char *label, size_t llen, const unsigned char *ctx, size_t ctxlen, char **err); + + int ngx_http_lua_ffi_req_shared_ssl_ciphers(ngx_http_request_t *r, + unsigned short *ciphers, unsigned short *nciphers, char **err); + + typedef struct { + uint16_t nciphers; + uint16_t ciphers[?]; + } ngx_lua_ssl_ciphers; ]] ngx_lua_ffi_ssl_set_der_certificate = @@ -143,6 +160,7 @@ if subsystem == 'http' then ngx_lua_ffi_ssl_export_keying_material_early = C.ngx_http_lua_ffi_ssl_export_keying_material_early ngx_lua_ffi_get_req_ssl_pointer = C.ngx_http_lua_ffi_get_req_ssl_pointer + ngx_lua_ffi_req_shared_ssl_ciphers = C.ngx_http_lua_ffi_req_shared_ssl_ciphers elseif subsystem == 'stream' then ffi.cdef[[ @@ -237,6 +255,384 @@ local charpp = ffi.new("char*[1]") local intp = ffi.new("int[1]") local ushortp = ffi.new("unsigned short[1]") +do + --https://datatracker.ietf.org/doc/html/rfc8701 + local TLS_GREASE = { + [2570] = true, + [6682] = true, + [10794] = true, + [14906] = true, + [19018] = true, + [23130] = true, + [27242] = true, + [31354] = true, + [35466] = true, + [39578] = true, + [43690] = true, + [47802] = true, + [51914] = true, + [56026] = true, + [60138] = true, + [64250] = true + } + + -- TLS cipher suite functionality + local tls_proto_id = { + -- TLS 1.3 ciphers + [0x1301] = { + iana_name = "TLS_AES_128_GCM_SHA256", + tls_version = 1.3, + kex = "any", + auth = "any", + enc = "AESGCM(128)", + hash = "AEAD" + }, + [0x1302] = { + iana_name = "TLS_AES_256_GCM_SHA384", + tls_version = 1.3, + kex = "any", + auth = "any", + enc = "AESGCM(256)", + hash = "AEAD" + }, + [0x1303] = { + iana_name = "TLS_CHACHA20_POLY1305_SHA256", + tls_version = 1.3, + kex = "any", + auth = "any", + enc = "CHACHA20/POLY1305(256)", + hash = "AEAD" + }, + [0x1304] = { + iana_name = "TLS_AES_128_CCM_SHA256", + tls_version = 1.3, + kex = "none", + auth = "none", + enc = "AES 128 CCM", + hash = "SHA256" + }, + [0x1305] = { + iana_name = "TLS_AES_128_CCM_8_SHA256", + tls_version = 1.3, + kex = "none", + auth = "none", + enc = "AES 128 CCM 8", + hash = "SHA256" + }, + -- TLS 1.2 ciphers (most common ones) + [0xc02b] = { + iana_name = "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + tls_version = 1.2, + kex = "ECDH", + auth = "ECDSA", + enc = "AESGCM(128)", + hash = "AEAD" + }, + [0xc02f] = { + iana_name = "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + tls_version = 1.2, + kex = "ECDH", + auth = "RSA", + enc = "AESGCM(128)", + hash = "AEAD" + }, + [0xc02c] = { + iana_name = "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", + tls_version = 1.2, + kex = "ECDH", + auth = "ECDSA", + enc = "AESGCM(256)", + hash = "AEAD" + }, + [0xc030] = { + iana_name = "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + tls_version = 1.2, + kex = "ECDH", + auth = "RSA", + enc = "AESGCM(256)", + hash = "AEAD" + }, + [0x9f] = { + iana_name = "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384", + tls_version = 1.2, + kex = "DH", + auth = "RSA", + enc = "AESGCM(256)", + hash = "AEAD" + }, + [0xcca9] = { + iana_name = "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", + tls_version = 1.2, + kex = "ECDH", + auth = "ECDSA", + enc = "CHACHA20/POLY1305(256)", + hash = "AEAD" + }, + [0xcca8] = { + iana_name = "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", + tls_version = 1.2, + kex = "ECDH", + auth = "RSA", + enc = "CHACHA20/POLY1305(256)", + hash = "AEAD" + }, + [0xccaa] = { + iana_name = "TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256", + tls_version = 1.2, + kex = "DH", + auth = "RSA", + enc = "CHACHA20/POLY1305(256)", + hash = "AEAD" + }, + [0xc02b] = { + iana_name = "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + tls_version = 1.2, + kex = "ECDH", + auth = "ECDSA", + enc = "AESGCM(128)", + hash = "AEAD" + }, + [0xc02f] = { + iana_name = "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + tls_version = 1.2, + kex = "ECDH", + auth = "RSA", + enc = "AESGCM(128)", + hash = "AEAD" + }, + [0x9e] = { + iana_name = "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256", + tls_version = 1.2, + kex = "DH", + auth = "RSA", + enc = "AESGCM(128)", + hash = "AEAD" + }, + [0xc024] = { + iana_name = "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384", + tls_version = 1.2, + kex = "ECDH", + auth = "ECDSA", + enc = "AES(256)", + hash = "SHA384" + }, + [0xc028] = { + iana_name = "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384", + tls_version = 1.2, + kex = "ECDH", + auth = "RSA", + enc = "AES(256)", + hash = "SHA384" + }, + [0x6b] = { + iana_name = "TLS_DHE_RSA_WITH_AES_256_CBC_SHA256", + tls_version = 1.2, + kex = "DH", + auth = "RSA", + enc = "AES(256)", + hash = "SHA256" + }, + [0xc023] = { + iana_name = "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256", + tls_version = 1.2, + kex = "ECDH", + auth = "ECDSA", + enc = "AES(128)", + hash = "SHA256" + }, + [0xc027] = { + iana_name = "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", + tls_version = 1.2, + kex = "ECDH", + auth = "RSA", + enc = "AES(128)", + hash = "SHA256" + }, + [0x67] = { + iana_name = "TLS_DHE_RSA_WITH_AES_128_CBC_SHA256", + tls_version = 1.2, + kex = "DH", + auth = "RSA", + enc = "AES(128)", + hash = "SHA256" + }, + [0xc00a] = { + iana_name = "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", + tls_version = 1.0, + kex = "ECDH", + auth = "ECDSA", + enc = "AES(256)", + hash = "SHA1" + }, + [0xc014] = { + iana_name = "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", + tls_version = 1.0, + kex = "ECDH", + auth = "RSA", + enc = "AES(256)", + hash = "SHA1" + }, + [0x39] = { + iana_name = "TLS_DHE_RSA_WITH_AES_256_CBC_SHA", + tls_version = 0x0300, + kex = "DH", + auth = "RSA", + enc = "AES(256)", + hash = "SHA1" + }, + [0xc009] = { + iana_name = "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", + tls_version = 1.0, + kex = "ECDH", + auth = "ECDSA", + enc = "AES(128)", + hash = "SHA1" + }, + [0xc013] = { + iana_name = "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", + tls_version = 1.0, + kex = "ECDH", + auth = "RSA", + enc = "AES(128)", + hash = "SHA1" + }, + [0x33] = { + iana_name = "TLS_DHE_RSA_WITH_AES_128_CBC_SHA", + tls_version = 0x0300, + kex = "DH", + auth = "RSA", + enc = "AES(128)", + hash = "SHA1" + }, + [0x9d] = { + iana_name = "TLS_RSA_WITH_AES_256_GCM_SHA384", + tls_version = 1.2, + kex = "RSA", + auth = "RSA", + enc = "AESGCM(256)", + hash = "AEAD" + }, + [0x9c] = { + iana_name = "TLS_RSA_WITH_AES_128_GCM_SHA256", + tls_version = 1.2, + kex = "RSA", + auth = "RSA", + enc = "AESGCM(128)", + hash = "AEAD" + }, + [0x3d] = { + iana_name = "TLS_RSA_WITH_AES_256_CBC_SHA256", + tls_version = 1.2, + kex = "RSA", + auth = "RSA", + enc = "AES(256)", + hash = "SHA256" + }, + [0x3c] = { + iana_name = "TLS_RSA_WITH_AES_128_CBC_SHA256", + tls_version = 1.2, + kex = "RSA", + auth = "RSA", + enc = "AES(128)", + hash = "SHA256" + }, + [0x35] = { + iana_name = "TLS_RSA_WITH_AES_256_CBC_SHA", + tls_version = 0x0300, + kex = "RSA", + auth = "RSA", + enc = "AES(256)", + hash = "SHA1" + }, + [0x2f] = { + iana_name = "TLS_RSA_WITH_AES_128_CBC_SHA", + tls_version = 0x0300, + kex = "RSA", + auth = "RSA", + enc = "AES(128)", + hash = "SHA1" + }, + -- 其他 PSK、SRP、RSA-PSK、DHE-PSK、ECDHE-PSK 可继续补充 + -- ... + } + + local unknown_cipher = { + iana_name = "UNKNOWN", + tls_version = 0, + kex = "UNKNOWN", + auth = "UNKNOWN", + enc = "UNKNOWN", + hash = "UNKNOWN" + } + + setmetatable(tls_proto_id, { + __index = function(t, k) + t[k] = unknown_cipher + return unknown_cipher + end + }) + + -- Iterator function for ciphers + local function iterate_ciphers(ciphers, n) + if n < ciphers.nciphers then + return n + 1, tls_proto_id[ciphers.ciphers[n]] + end + end + + -- Buffer for temporary cipher table conversion + local ciphers_t = {} + + -- Metatype for cipher structure + ffi.metatype('ngx_lua_ssl_ciphers', { + __ipairs = function(ciphers) + return iterate_ciphers, ciphers, 0 + end, + __tostring = function(ciphers) + for n, c in ipairs(ciphers) do + ciphers_t[n] = type(c) == "table" and c.iana_name or format("0x%.4x", c) + end + return concat(ciphers_t, ":", 1, ciphers.nciphers) + end + }) + + -- Cipher type and buffer + local ciphers_typ = ffi_typeof("ngx_lua_ssl_ciphers") + local ciphers_buf = ffi_new("uint16_t [?]", 256) + + function _M.get_shared_ssl_ciphers() + local r = get_request() + if not r then + error("no request found") + end + + ciphers_buf[0] = 255 -- Set max number of ciphers we can hold + local rc = ngx_lua_ffi_req_shared_ssl_ciphers(r, ciphers_buf + 1, ciphers_buf, errmsg) + if rc ~= FFI_OK then + return nil, ffi_str(errmsg[0]) + end + + -- Filter out GREASE ciphers + local filtered_count = 0 + local filtered_buf = ffi_new("uint16_t [?]", ciphers_buf[0] + 1) + + for i = 1, ciphers_buf[0] do + local cipher_id = ciphers_buf[i] + if not TLS_GREASE[cipher_id] then + filtered_buf[filtered_count + 1] = cipher_id + filtered_count = filtered_count + 1 + end + end + + filtered_buf[0] = filtered_count + + -- Create the cipher structure + local ciphers = ciphers_typ(filtered_count) + ffi_copy(ciphers, filtered_buf, (filtered_count + 1) * ffi_sizeof('uint16_t')) + + return ciphers + end +end + function _M.clear_certs() local r = get_request() diff --git a/lib/ngx/ssl.md b/lib/ngx/ssl.md index d4d2c1c94..54ff98d87 100644 --- a/lib/ngx/ssl.md +++ b/lib/ngx/ssl.md @@ -32,6 +32,7 @@ Table of Contents * [set_priv_key](#set_priv_key) * [verify_client](#verify_client) * [get_client_random](#get_client_random) + * [get_shared_ssl_ciphers](#get_shared_ssl_ciphers) * [get_req_ssl_pointer](#get_req_ssl_pointer) * [Community](#community) * [English Mailing List](#english-mailing-list) @@ -651,6 +652,52 @@ This function can be called in any context where downstream https is used, but i [Back to TOC](#table-of-contents) +get_shared_ssl_ciphers +----------- +**syntax:** *ciphers = ssl.get_shared_ssl_ciphers()* + +**context:** *any* + +Returns a structured object containing the cipher suite information that are supported by both the server and the client for the current SSL connection. + +This function returns the intersection of server-supported ciphers and client-offered ciphers, representing the ciphers that can actually be used for the connection. + +The returned object is a structured FFI object with the following characteristics: + +- `ciphers.nciphers`: The number of shared ciphers +- Supports `ipairs()` iteration to access detailed cipher information +- Supports `tostring()` to get a formatted cipher list +- Each cipher entry contains: + - `iana_name`: The IANA standard name (e.g., "TLS_AES_128_GCM_SHA256") + - `tls_version`: The TLS version (1.2, 1.3, etc.) + - `kex`: Key exchange algorithm (e.g., "ECDHE") + - `auth`: Authentication method (e.g., "RSA", "ECDSA") + - `enc`: Encryption algorithm (e.g., "AES 128 GCM") + - `hash`: Hash algorithm (e.g., "SHA256") + +Example usage: +```lua +local ciphers = ssl.get_shared_ssl_ciphers() +if ciphers then + ngx.log(ngx.INFO, "Found ", ciphers.nciphers, " shared ciphers") + for i, cipher in ipairs(ciphers) do + ngx.log(ngx.INFO, "Cipher: ", cipher.iana_name, + " (TLS ", cipher.tls_version, ")") + end +end +``` + +GREASE (Generate Random Extensions And Sustain Extensibility) cipher values are automatically filtered out from the results. + +Returns `nil` and an error string on failure. + +This function can be called in any context where downstream https is used. + +This function was first added in version `0.1.29`. + +[Back to TOC](#table-of-contents) + + get_req_ssl_pointer ------------ **syntax:** *ssl_ptr, err = ssl.get_req_ssl_pointer()* diff --git a/t/ssl.t b/t/ssl.t index 6dc9ea350..dd795a1b0 100644 --- a/t/ssl.t +++ b/t/ssl.t @@ -3419,3 +3419,63 @@ SUCCESS [error] [alert] [emerg] + + +=== TEST 33: get shared SSL ciphers +--- http_config + lua_package_path "$TEST_NGINX_LUA_PACKAGE_PATH"; + server { + listen unix:$TEST_NGINX_HTML_DIR/nginx.sock ssl; + server_name test.com; + ssl_protocols TLSv1.2; + ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384; + + ssl_certificate_by_lua_block { + local ssl = require "ngx.ssl" + local ciphers, err = ssl.get_shared_ssl_ciphers() + if not err and ciphers then + ngx.log(ngx.INFO, "shared ciphers count: ", ciphers.nciphers) + ngx.log(ngx.INFO, "cipher summary: ", tostring(ciphers)) + local count = 0 + for i, cipher_info in ipairs(ciphers) do + count = count + 1 + ngx.log(ngx.INFO, i, ": SHARED_CIPHER ", cipher_info.iana_name) + if count >= 3 then -- log only first 3 to avoid too much output + break + end + end + else + ngx.log(ngx.ERR, "failed to get shared ciphers: ", err) + end + } + ssl_certificate ../../cert/test.crt; + ssl_certificate_key ../../cert/test.key; + + server_tokens off; + location /foo { + default_type 'text/plain'; + content_by_lua_block {ngx.status = 200 ngx.say("foo") ngx.exit(200)} + more_clear_headers Date; + } + } +--- config + location /t { + proxy_ssl_protocols TLSv1.2; + proxy_ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256; + proxy_pass https://unix:$TEST_NGINX_HTML_DIR/nginx.sock:/foo; + proxy_ssl_session_reuse off; + } + +--- request +GET /t +--- response_body +foo +--- error_log eval +[qr/shared ciphers count: 3/, +qr/1: SHARED_CIPHER TLS_/] +--- error_log chomp +cipher summary: TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 +--- no_error_log +[alert] +[crit] +[error] \ No newline at end of file From bcb86aa00b9cb525d04b1e48d578acfbdf457ff6 Mon Sep 17 00:00:00 2001 From: Sunny Chan Date: Wed, 18 Jun 2025 02:02:36 +0000 Subject: [PATCH 2/3] check: format --- lib/ngx/ssl.lua | 23 +---------------------- 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/lib/ngx/ssl.lua b/lib/ngx/ssl.lua index 3ce86c223..4a514d2c9 100644 --- a/lib/ngx/ssl.lua +++ b/lib/ngx/ssl.lua @@ -24,8 +24,6 @@ local get_size_ptr = base.get_size_ptr local FFI_DECLINED = base.FFI_DECLINED local FFI_OK = base.FFI_OK local subsystem = ngx.config.subsystem -local table_new = require "table.new" -local table_insert = table.insert local ngx_lua_ffi_ssl_set_der_certificate @@ -384,22 +382,6 @@ do enc = "CHACHA20/POLY1305(256)", hash = "AEAD" }, - [0xc02b] = { - iana_name = "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", - tls_version = 1.2, - kex = "ECDH", - auth = "ECDSA", - enc = "AESGCM(128)", - hash = "AEAD" - }, - [0xc02f] = { - iana_name = "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", - tls_version = 1.2, - kex = "ECDH", - auth = "RSA", - enc = "AESGCM(128)", - hash = "AEAD" - }, [0x9e] = { iana_name = "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256", tls_version = 1.2, @@ -614,7 +596,7 @@ do -- Filter out GREASE ciphers local filtered_count = 0 local filtered_buf = ffi_new("uint16_t [?]", ciphers_buf[0] + 1) - + for i = 1, ciphers_buf[0] do local cipher_id = ciphers_buf[i] if not TLS_GREASE[cipher_id] then @@ -622,7 +604,6 @@ do filtered_count = filtered_count + 1 end end - filtered_buf[0] = filtered_count -- Create the cipher structure @@ -633,7 +614,6 @@ do end end - function _M.clear_certs() local r = get_request() if not r then @@ -648,7 +628,6 @@ function _M.clear_certs() return nil, ffi_str(errmsg[0]) end - function _M.set_der_cert(data) local r = get_request() if not r then From e98f40d3bb106e264726ffb79ce649dedf62f51c Mon Sep 17 00:00:00 2001 From: Sunny Chan Date: Wed, 18 Jun 2025 02:12:45 +0000 Subject: [PATCH 3/3] fix: format --- lib/ngx/ssl.lua | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/ngx/ssl.lua b/lib/ngx/ssl.lua index 4a514d2c9..2c6c51851 100644 --- a/lib/ngx/ssl.lua +++ b/lib/ngx/ssl.lua @@ -158,7 +158,8 @@ if subsystem == 'http' then ngx_lua_ffi_ssl_export_keying_material_early = C.ngx_http_lua_ffi_ssl_export_keying_material_early ngx_lua_ffi_get_req_ssl_pointer = C.ngx_http_lua_ffi_get_req_ssl_pointer - ngx_lua_ffi_req_shared_ssl_ciphers = C.ngx_http_lua_ffi_req_shared_ssl_ciphers + ngx_lua_ffi_req_shared_ssl_ciphers = + C.ngx_http_lua_ffi_req_shared_ssl_ciphers elseif subsystem == 'stream' then ffi.cdef[[ @@ -571,7 +572,8 @@ do end, __tostring = function(ciphers) for n, c in ipairs(ciphers) do - ciphers_t[n] = type(c) == "table" and c.iana_name or format("0x%.4x", c) + ciphers_t[n] = type(c) == "table" and c.iana_name or + format("0x%.4x", c) end return concat(ciphers_t, ":", 1, ciphers.nciphers) end @@ -588,7 +590,8 @@ do end ciphers_buf[0] = 255 -- Set max number of ciphers we can hold - local rc = ngx_lua_ffi_req_shared_ssl_ciphers(r, ciphers_buf + 1, ciphers_buf, errmsg) + local rc = ngx_lua_ffi_req_shared_ssl_ciphers(r, ciphers_buf + 1, + ciphers_buf, errmsg) if rc ~= FFI_OK then return nil, ffi_str(errmsg[0]) end @@ -608,7 +611,8 @@ do -- Create the cipher structure local ciphers = ciphers_typ(filtered_count) - ffi_copy(ciphers, filtered_buf, (filtered_count + 1) * ffi_sizeof('uint16_t')) + ffi_copy(ciphers, filtered_buf, + (filtered_count + 1) * ffi_sizeof('uint16_t')) return ciphers end @@ -628,6 +632,7 @@ function _M.clear_certs() return nil, ffi_str(errmsg[0]) end + function _M.set_der_cert(data) local r = get_request() if not r then