diff --git a/lib/ngx/ssl/clienthello.lua b/lib/ngx/ssl/clienthello.lua index 2aef74b60..331860c41 100644 --- a/lib/ngx/ssl/clienthello.lua +++ b/lib/ngx/ssl/clienthello.lua @@ -22,12 +22,14 @@ local lshift = bit.lshift local table_insert = table.insert local table_new = require "table.new" local intp = ffi.new("int*[1]") +local usp = ffi.new("unsigned short*[1]") local ngx_lua_ffi_ssl_get_client_hello_server_name local ngx_lua_ffi_ssl_get_client_hello_ext local ngx_lua_ffi_ssl_set_protocols local ngx_lua_ffi_ssl_get_client_hello_ext_present +local ngx_lua_ffi_ssl_get_client_hello_ciphers if subsystem == 'http' then @@ -41,8 +43,13 @@ if subsystem == 'http' then int ngx_http_lua_ffi_ssl_set_protocols(ngx_http_request_t *r, int protocols, char **err); + int ngx_http_lua_ffi_ssl_get_client_hello_ext_present(ngx_http_request_t *r, int **extensions, size_t *extensions_len, char **err); + /* Undefined for the stream subsystem */ + int ngx_http_lua_ffi_ssl_get_client_hello_ciphers(ngx_http_request_t *r, + int **ciphers, size_t *cipherslen, char **err); + /* Undefined for the stream subsystem */ ]] ngx_lua_ffi_ssl_get_client_hello_server_name = @@ -52,6 +59,9 @@ if subsystem == 'http' then ngx_lua_ffi_ssl_set_protocols = C.ngx_http_lua_ffi_ssl_set_protocols ngx_lua_ffi_ssl_get_client_hello_ext_present = C.ngx_http_lua_ffi_ssl_get_client_hello_ext_present + ngx_lua_ffi_ssl_get_client_hello_ciphers = + C.ngx_http_lua_ffi_ssl_get_client_hello_ciphers + elseif subsystem == 'stream' then @@ -83,6 +93,27 @@ local ccharpp = ffi.new("const char*[1]") local cucharpp = ffi.new("const unsigned char*[1]") +--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 +} + + -- return server_name, err function _M.get_client_hello_server_name() local r = get_request() @@ -125,6 +156,8 @@ function _M.get_client_hello_ext_present() local rc = ngx_lua_ffi_ssl_get_client_hello_ext_present(r, intp, sizep, errmsg) +-- the function used under the hood, SSL_client_hello_get1_extensions_present, +-- already excludes GREASE, thank G*d if rc == FFI_OK then -- Convert C array to Lua table local array = intp[0] local size = tonumber(sizep[0]) @@ -144,6 +177,45 @@ function _M.get_client_hello_ext_present() return nil, ffi_str(errmsg[0]) end +-- return ciphers_table, err +-- excluding GREASE ciphers +function _M.get_client_hello_ciphers() + local r = get_request() + if not r then + error("no request found") + end + + if ngx_phase() ~= "ssl_client_hello" then + error("API disabled in the current context") + end + + local sizep = get_size_ptr() + + local rc = ngx_lua_ffi_ssl_get_client_hello_ciphers(r, usp, + sizep, errmsg) + if rc == FFI_OK then + local ciphers_table = table_new(16, 0) + local array = usp[0] + local size = tonumber(sizep[0]) + local y = 1 + for i=0, size-1, 1 do + if not TLS_GREASE[array[i]] then + ciphers_table[y] = array[i] + y = y + 1 + end + end + + return ciphers_table + end + + -- NGX_DECLINED + if rc == -5 then + return nil + end + + return nil, ffi_str(errmsg[0]) +end + -- return ext, err function _M.get_client_hello_ext(ext_type) local r = get_request() diff --git a/lib/ngx/ssl/clienthello.md b/lib/ngx/ssl/clienthello.md index 4f4b90958..05bc4c9a2 100644 --- a/lib/ngx/ssl/clienthello.md +++ b/lib/ngx/ssl/clienthello.md @@ -13,6 +13,7 @@ Table of Contents * [Methods](#methods) * [get_client_hello_server_name](#get_client_hello_server_name) * [get_supported_versions](#get_supported_versions) + * [get_client_hello_ciphers](#get_client_hello_ciphers) * [get_client_hello_ext_present](#get_client_hello_ext_present) * [get_client_hello_ext](#get_client_hello_ext) * [set_protocols](#set_protocols) @@ -126,6 +127,44 @@ So this function can only be called in the context of [ssl_client_hello_by_lua*] [Back to TOC](#table-of-contents) +get_client_hello_ciphers +---------------------------- +**syntax:** *ciphers, err = ssl_clt.get_client_hello_ciphers()* + +**context:** *ssl_client_hello_by_lua** + +Returns a Lua table containing the decimal representations of the ciphers sent by the client on success. + +GREASE ciphers are also returned by the underlying OPENSSL function (SSL_client_hello_get0_ciphers) but excluded by the lua implementation of get_client_hello_ciphers(). + +In case of errors, `nil` and a string describing the error are returned. + +This function can only be called in the context of [ssl_client_hello_by_lua*](https://github.com/openresty/lua-nginx-module/#ssl_client_hello_by_lua_block). + +Example: + +```nginx +# nginx.conf +server { + listen 443 ssl; + server_name test.com; + ssl_client_hello_by_lua_block { + local ssl_clt = require "ngx.ssl.clienthello" + local ciphers, err = ssl_clt.get_client_hello_ciphers() + if not ciphers then + ngx.log(ngx.ERR, "failed to get_client_hello_ciphers()") + ngx.exit(ngx.ERROR) + end + + for i, cipher in ipairs(ciphers) do + ngx.log(ngx.INFO, "ciphers ", cipher) + end + } + ssl_certificate test.crt; + ssl_certificate_key test.key; +} +``` + get_client_hello_ext_present ---------------------------- **syntax:** *ext, err = ssl_clt.get_client_hello_ext_present()* @@ -140,6 +179,11 @@ Note that the ext is gotten from the raw extensions of the client hello message So this function can only be called in the context of [ssl_client_hello_by_lua*](https://github.com/openresty/lua-nginx-module/#ssl_client_hello_by_lua_block). +GREASE extensions are excluded by the underlying OPENSSL function (SSL_client_hello_get1_extensions_present) + +Most modern browsers will randomize the order of the extensions so you may want to sort the table before working with it. + + Example: ```nginx diff --git a/t/ssl-client-hello.t b/t/ssl-client-hello.t index ecc25d688..a504df5bf 100644 --- a/t/ssl-client-hello.t +++ b/t/ssl-client-hello.t @@ -1065,3 +1065,102 @@ qr/1: TLS EXT \d+, context: ssl_client_hello_by_lua/ [alert] [crit] [placeholder] + + + +=== TEST 11: log ciphers in the clienthello packet +--- http_config + lua_package_path "$TEST_NGINX_LUA_PACKAGE_PATH"; + + server { + listen 127.0.0.2:$TEST_NGINX_RAND_PORT_1 ssl; + server_name test.com; + ssl_client_hello_by_lua_block { + local ssl_clt = require "ngx.ssl.clienthello" + local ciphers, err = ssl_clt.get_client_hello_ciphers() + if not err and ciphers then + for i, cipher in ipairs(ciphers) do + ngx.log(ngx.INFO, i, ": CIPHER ", cipher) + end + else + ngx.log(ngx.ERR, "failed to get ciphers") + end + ngx.exit(ngx.ERROR) + } + + ssl_protocols TLSv1 TLSv1.1 TLSv1.2; + 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 = 201 ngx.say("foo") ngx.exit(201)} + more_clear_headers Date; + } + } +--- config + server_tokens off; + lua_ssl_trusted_certificate ../../cert/test.crt; + lua_ssl_protocols TLSv1 TLSv1.1 TLSv1.2; + + location /t { + content_by_lua_block { + do + local sock = ngx.socket.tcp() + + sock:settimeout(3000) + + local ok, err = sock:connect("127.0.0.2", $TEST_NGINX_RAND_PORT_1) + if not ok then + ngx.say("failed to connect: ", err) + return + end + + ngx.say("connected: ", ok) + + local sess, err = sock:sslhandshake(nil, nil, true) + if not sess then + ngx.say("failed to do SSL handshake: ", err) + return + end + + ngx.say("ssl handshake: ", type(sess)) + + local req = "GET /foo HTTP/1.0\r\nHost: test.com\r\nConnection: close\r\n\r\n" + local bytes, err = sock:send(req) + if not bytes then + ngx.say("failed to send http request: ", err) + return + end + + ngx.say("sent http request: ", bytes, " bytes.") + + while true do + local line, err = sock:receive() + if not line then + -- ngx.say("failed to receive response status line: ", err) + break + end + + ngx.say("received: ", line) + end + + local ok, err = sock:close() + ngx.say("close: ", ok, " ", err) + end -- do + -- collectgarbage() + } + } + +--- request +GET /t +--- response_body +connected: 1 +failed to do SSL handshake: handshake failed +--- error_log eval +qr/1: CIPHER \d+, context: ssl_client_hello_by_lua/ +--- no_error_log +[alert] +[crit] +[placeholder]