diff --git a/lib/ngx/ssl.lua b/lib/ngx/ssl.lua index b696beaf4..9a961aef0 100644 --- a/lib/ngx/ssl.lua +++ b/lib/ngx/ssl.lua @@ -9,9 +9,15 @@ 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 @@ -43,6 +49,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 +121,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, int filter_grease, char **err); + + typedef struct { + uint16_t nciphers; + uint16_t ciphers[?]; + } ngx_lua_ssl_ciphers; ]] ngx_lua_ffi_ssl_set_der_certificate = @@ -143,6 +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 elseif subsystem == 'stream' then ffi.cdef[[ @@ -237,6 +254,37 @@ local charpp = ffi.new("char*[1]") local intp = ffi.new("int[1]") local ushortp = ffi.new("unsigned short[1]") +do + local ciphers_buf = ffi_new("uint16_t [?]", 256) + + function _M.get_shared_ssl_ciphers(filter_grease) + local r = get_request() + if not r then + error("no request found") + end + + if filter_grease == nil then + filter_grease = true -- Default to filter GREASE + end + + ciphers_buf[0] = 255 -- Set max number of ciphers we can hold + local filter_flag = filter_grease and 1 or 0 + local rc = ngx_lua_ffi_req_shared_ssl_ciphers(r, ciphers_buf + 1, + ciphers_buf, filter_flag, errmsg) + if rc ~= FFI_OK then + return nil, ffi_str(errmsg[0]) + end + + -- Build result table + local result = {} + for i = 1, ciphers_buf[0] do + local cipher_id = ciphers_buf[i] + result[#result + 1] = cipher_id + end + + return result + end +end function _M.clear_certs() local r = get_request() diff --git a/lib/ngx/ssl.md b/lib/ngx/ssl.md index d4d2c1c94..bdd84b0c8 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,20 +652,50 @@ This function can be called in any context where downstream https is used, but i [Back to TOC](#table-of-contents) -get_req_ssl_pointer ------------- -**syntax:** *ssl_ptr, err = ssl.get_req_ssl_pointer()* +get_shared_ssl_ciphers +----------- +**syntax:** *ciphers = ssl.get_shared_ssl_ciphers()* **context:** *any* -Retrieves the OpenSSL `SSL*` object for the current downstream connection. +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: -Returns an FFI pointer on success, or a `nil` value and a string describing the error otherwise. +- `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") -If you need to retain the pointer beyond the current phase then you will need to use OpenSSL's `SSL_up_ref` to increase the reference count. -If you do, ensure that your reference is released with `SSL_free`. +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 +``` -This function was first added in version `0.1.16`. +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) [Back to TOC](#table-of-contents) diff --git a/t/ssl.t b/t/ssl.t index 02ec0a913..e738da023 100644 --- a/t/ssl.t +++ b/t/ssl.t @@ -8,7 +8,7 @@ use t::TestCore; repeat_each(2); -plan tests => repeat_each() * (blocks() * 6 - 1); +plan tests => repeat_each() * (blocks() * 6 ); no_long_string(); #no_diff(); @@ -110,7 +110,7 @@ failed to do SSL handshake: handshake failed --- error_log eval ['lua ssl server name: "test.com"', -qr/routines::no suitable signature algorithm|sslv3 alert handshake failure|routines:OPENSSL_internal:SSLV3_ALERT_HANDSHAKE_FAILURE:SSL alert number 40/] +qr/sslv3 alert handshake failure|routines:OPENSSL_internal:SSLV3_ALERT_HANDSHAKE_FAILURE:SSL alert number 40/] --- no_error_log [alert] @@ -3419,3 +3419,61 @@ SUCCESS [error] [alert] [emerg] + + + +=== TEST 35: 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) + local count = 0 + for i, cipher_id in ipairs(ciphers) do + count = count + 1 + ngx.log(ngx.INFO, string.format("%d: SHARED_CIPHER 0x%04x", i, cipher_id)) + 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: \d+/, +qr/1: SHARED_CIPHER 0x/] +--- no_error_log +[alert] +[crit] +[error] \ No newline at end of file