Skip to content

Commit 7482bd7

Browse files
committed
feature: ngx_lua_ffi_ssl_get_client_hello_ciphers()
1 parent 8193d5e commit 7482bd7

File tree

3 files changed

+214
-0
lines changed

3 files changed

+214
-0
lines changed

lib/ngx/ssl/clienthello.lua

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,14 @@ local lshift = bit.lshift
2222
local table_insert = table.insert
2323
local table_new = require "table.new"
2424
local intp = ffi.new("int*[1]")
25+
local usp = ffi.new("unsigned short*[1]")
2526

2627

2728
local ngx_lua_ffi_ssl_get_client_hello_server_name
2829
local ngx_lua_ffi_ssl_get_client_hello_ext
2930
local ngx_lua_ffi_ssl_set_protocols
3031
local ngx_lua_ffi_ssl_get_client_hello_ext_present
32+
local ngx_lua_ffi_ssl_get_client_hello_ciphers
3133

3234

3335
if subsystem == 'http' then
@@ -41,8 +43,13 @@ if subsystem == 'http' then
4143

4244
int ngx_http_lua_ffi_ssl_set_protocols(ngx_http_request_t *r,
4345
int protocols, char **err);
46+
4447
int ngx_http_lua_ffi_ssl_get_client_hello_ext_present(ngx_http_request_t *r,
4548
int **extensions, size_t *extensions_len, char **err);
49+
/* Undefined for the stream subsystem */
50+
int ngx_http_lua_ffi_ssl_get_client_hello_ciphers(ngx_http_request_t *r,
51+
int **ciphers, size_t *cipherslen, char **err);
52+
/* Undefined for the stream subsystem */
4653
]]
4754

4855
ngx_lua_ffi_ssl_get_client_hello_server_name =
@@ -52,6 +59,9 @@ if subsystem == 'http' then
5259
ngx_lua_ffi_ssl_set_protocols = C.ngx_http_lua_ffi_ssl_set_protocols
5360
ngx_lua_ffi_ssl_get_client_hello_ext_present =
5461
C.ngx_http_lua_ffi_ssl_get_client_hello_ext_present
62+
ngx_lua_ffi_ssl_get_client_hello_ciphers =
63+
C.ngx_http_lua_ffi_ssl_get_client_hello_ciphers
64+
5565

5666

5767
elseif subsystem == 'stream' then
@@ -83,6 +93,27 @@ local ccharpp = ffi.new("const char*[1]")
8393
local cucharpp = ffi.new("const unsigned char*[1]")
8494

8595

96+
--https://datatracker.ietf.org/doc/html/rfc8701
97+
local TLS_GREASE = {
98+
[2570] = true,
99+
[6682] = true,
100+
[10794] = true,
101+
[14906] = true,
102+
[19018] = true,
103+
[23130] = true,
104+
[27242] = true,
105+
[31354] = true,
106+
[35466] = true,
107+
[39578] = true,
108+
[43690] = true,
109+
[47802] = true,
110+
[51914] = true,
111+
[56026] = true,
112+
[60138] = true,
113+
[64250] = true
114+
}
115+
116+
86117
-- return server_name, err
87118
function _M.get_client_hello_server_name()
88119
local r = get_request()
@@ -125,6 +156,8 @@ function _M.get_client_hello_ext_present()
125156

126157
local rc = ngx_lua_ffi_ssl_get_client_hello_ext_present(r, intp,
127158
sizep, errmsg)
159+
-- the function used under the hood, SSL_client_hello_get1_extensions_present,
160+
-- already excludes GREASE, thank G*d
128161
if rc == FFI_OK then -- Convert C array to Lua table
129162
local array = intp[0]
130163
local size = tonumber(sizep[0])
@@ -144,6 +177,45 @@ function _M.get_client_hello_ext_present()
144177
return nil, ffi_str(errmsg[0])
145178
end
146179

180+
-- return ciphers_table, err
181+
-- excluding GREASE ciphers
182+
function _M.get_client_hello_ciphers()
183+
local r = get_request()
184+
if not r then
185+
error("no request found")
186+
end
187+
188+
if ngx_phase() ~= "ssl_client_hello" then
189+
error("API disabled in the current context")
190+
end
191+
192+
local sizep = get_size_ptr()
193+
194+
local rc = ngx_lua_ffi_ssl_get_client_hello_ciphers(r, usp,
195+
sizep, errmsg)
196+
if rc == FFI_OK then
197+
local ciphers_table = table_new(16, 0)
198+
local array = usp[0]
199+
local size = tonumber(sizep[0])
200+
local y = 1
201+
for i=0, size-1, 1 do
202+
if not TLS_GREASE[array[i]] then
203+
ciphers_table[y] = array[i]
204+
y = y + 1
205+
end
206+
end
207+
208+
return ciphers_table
209+
end
210+
211+
-- NGX_DECLINED
212+
if rc == -5 then
213+
return nil
214+
end
215+
216+
return nil, ffi_str(errmsg[0])
217+
end
218+
147219
-- return ext, err
148220
function _M.get_client_hello_ext(ext_type)
149221
local r = get_request()

lib/ngx/ssl/clienthello.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Table of Contents
1313
* [Methods](#methods)
1414
* [get_client_hello_server_name](#get_client_hello_server_name)
1515
* [get_supported_versions](#get_supported_versions)
16+
* [get_client_hello_ciphers](#get_client_hello_ciphers)
1617
* [get_client_hello_ext_present](#get_client_hello_ext_present)
1718
* [get_client_hello_ext](#get_client_hello_ext)
1819
* [set_protocols](#set_protocols)
@@ -126,6 +127,44 @@ So this function can only be called in the context of [ssl_client_hello_by_lua*]
126127

127128
[Back to TOC](#table-of-contents)
128129

130+
get_client_hello_ciphers
131+
----------------------------
132+
**syntax:** *ciphers, err = ssl_clt.get_client_hello_ciphers()*
133+
134+
**context:** *ssl_client_hello_by_lua**
135+
136+
Returns a Lua table containing the decimal representations of the ciphers sent by the client on success.
137+
138+
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().
139+
140+
In case of errors, `nil` and a string describing the error are returned.
141+
142+
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).
143+
144+
Example:
145+
146+
```nginx
147+
# nginx.conf
148+
server {
149+
listen 443 ssl;
150+
server_name test.com;
151+
ssl_client_hello_by_lua_block {
152+
local ssl_clt = require "ngx.ssl.clienthello"
153+
local ciphers, err = ssl_clt.get_client_hello_ciphers()
154+
if not ciphers then
155+
ngx.log(ngx.ERR, "failed to get_client_hello_ciphers()")
156+
ngx.exit(ngx.ERROR)
157+
end
158+
159+
for i, cipher in ipairs(ciphers) do
160+
ngx.log(ngx.INFO, "ciphers ", cipher)
161+
end
162+
}
163+
ssl_certificate test.crt;
164+
ssl_certificate_key test.key;
165+
}
166+
```
167+
129168
get_client_hello_ext_present
130169
----------------------------
131170
**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
140179

141180
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).
142181

182+
GREASE extensions are excluded by the underlying OPENSSL function (SSL_client_hello_get1_extensions_present)
183+
184+
Most modern browsers will randomize the order of the extensions so you may want to sort the table before working with it.
185+
186+
143187
Example:
144188

145189
```nginx

t/ssl-client-hello.t

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1065,3 +1065,101 @@ qr/1: TLS EXT \d+, context: ssl_client_hello_by_lua/
10651065
[alert]
10661066
[crit]
10671067
[placeholder]
1068+
1069+
1070+
=== TEST 11: log ciphers in the clienthello packet
1071+
--- http_config
1072+
lua_package_path "$TEST_NGINX_LUA_PACKAGE_PATH";
1073+
1074+
server {
1075+
listen 127.0.0.2:$TEST_NGINX_RAND_PORT_1 ssl;
1076+
server_name test.com;
1077+
ssl_client_hello_by_lua_block {
1078+
local ssl_clt = require "ngx.ssl.clienthello"
1079+
local ciphers, err = ssl_clt.get_client_hello_ciphers()
1080+
if not err and ciphers then
1081+
for i, cipher in ipairs(ciphers) do
1082+
ngx.log(ngx.INFO, i, ": CIPHER ", cipher)
1083+
end
1084+
else
1085+
ngx.log(ngx.ERR, "failed to get ciphers")
1086+
end
1087+
ngx.exit(ngx.ERROR)
1088+
}
1089+
1090+
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
1091+
ssl_certificate ../../cert/test.crt;
1092+
ssl_certificate_key ../../cert/test.key;
1093+
1094+
server_tokens off;
1095+
location /foo {
1096+
default_type 'text/plain';
1097+
content_by_lua_block {ngx.status = 201 ngx.say("foo") ngx.exit(201)}
1098+
more_clear_headers Date;
1099+
}
1100+
}
1101+
--- config
1102+
server_tokens off;
1103+
lua_ssl_trusted_certificate ../../cert/test.crt;
1104+
lua_ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
1105+
1106+
location /t {
1107+
content_by_lua_block {
1108+
do
1109+
local sock = ngx.socket.tcp()
1110+
1111+
sock:settimeout(3000)
1112+
1113+
local ok, err = sock:connect("127.0.0.2", $TEST_NGINX_RAND_PORT_1)
1114+
if not ok then
1115+
ngx.say("failed to connect: ", err)
1116+
return
1117+
end
1118+
1119+
ngx.say("connected: ", ok)
1120+
1121+
local sess, err = sock:sslhandshake(nil, nil, true)
1122+
if not sess then
1123+
ngx.say("failed to do SSL handshake: ", err)
1124+
return
1125+
end
1126+
1127+
ngx.say("ssl handshake: ", type(sess))
1128+
1129+
local req = "GET /foo HTTP/1.0\r\nHost: test.com\r\nConnection: close\r\n\r\n"
1130+
local bytes, err = sock:send(req)
1131+
if not bytes then
1132+
ngx.say("failed to send http request: ", err)
1133+
return
1134+
end
1135+
1136+
ngx.say("sent http request: ", bytes, " bytes.")
1137+
1138+
while true do
1139+
local line, err = sock:receive()
1140+
if not line then
1141+
-- ngx.say("failed to receive response status line: ", err)
1142+
break
1143+
end
1144+
1145+
ngx.say("received: ", line)
1146+
end
1147+
1148+
local ok, err = sock:close()
1149+
ngx.say("close: ", ok, " ", err)
1150+
end -- do
1151+
-- collectgarbage()
1152+
}
1153+
}
1154+
1155+
--- request
1156+
GET /t
1157+
--- response_body
1158+
connected: 1
1159+
failed to do SSL handshake: handshake failed
1160+
--- error_log eval
1161+
qr/1: CIPHER \d+, context: ssl_client_hello_by_lua/
1162+
--- no_error_log
1163+
[alert]
1164+
[crit]
1165+
[placeholder]

0 commit comments

Comments
 (0)