Skip to content

feature: get_client_hello_ciphers() #498

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions lib/ngx/ssl/clienthello.lua
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,16 @@ local lshift = bit.lshift
local table_insert = table.insert
local table_new = require "table.new"
local intp = ffi.new("int*[1]")
local get_string_buf = base.get_string_buf
local ffi_ushort_pointer_type = ffi.typeof("unsigned short *")
local ffi_cast = ffi.cast


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
Expand All @@ -41,8 +45,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,
unsigned short *ciphers, size_t ciphers_len, char **err);
/* Undefined for the stream subsystem */
]]

ngx_lua_ffi_ssl_get_client_hello_server_name =
Expand All @@ -52,6 +61,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
Expand Down Expand Up @@ -83,6 +95,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()
Expand Down Expand Up @@ -125,6 +158,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])
Expand All @@ -144,6 +179,44 @@ 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 buf = get_string_buf(256) -- 256 bytes is short[128]
local ciphers = ffi_cast(ffi_ushort_pointer_type, buf)
local cipher_cnt = ngx_lua_ffi_ssl_get_client_hello_ciphers(r, ciphers,
128, errmsg)
if cipher_cnt > 0 then
local ciphers_table = table_new(16, 0)
local y = 1
for i = 0, cipher_cnt - 1 do
local cipher = tonumber(ciphers[i])
if not TLS_GREASE[cipher] then
ciphers_table[y] = cipher
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()
Expand Down
44 changes: 44 additions & 0 deletions lib/ngx/ssl/clienthello.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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()*
Expand All @@ -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
Expand Down
76 changes: 76 additions & 0 deletions t/ssl-client-hello.t
Original file line number Diff line number Diff line change
Expand Up @@ -1065,3 +1065,79 @@ 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
}

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))
end -- do
-- collectgarbage()
}
}

--- request
GET /t
--- response_body
connected: 1
ssl handshake: cdata
--- error_log eval
qr/1: CIPHER \d+, context: ssl_client_hello_by_lua/
--- no_error_log
[alert]
[crit]
[error]