Skip to content

Commit ad5ac4e

Browse files
committed
feature: add tcpsock:settrustedstore() for per-handshake trusted CAs
Adds a Lua wrapper around the new ngx_http_lua_ffi_socket_tcp_settrustedstore FFI in lua-nginx-module, exposed as tcpsock:settrustedstore(store). The store is an X509_STORE * cdata (e.g. from lua-resty-openssl) that overrides lua_ssl_trusted_certificate for the next sslhandshake() on this cosocket. The C side consumes the slot during the handshake, so the override does not leak across handshakes; passing nil clears it on both the lua and C sides so a previously-set store cannot dangle past a GC of the user's last reference. This is needed for per-request mTLS upstreams where the trusted CA set is determined dynamically (per-tenant routing, dynamic CA discovery) and cannot be expressed via the static lua_ssl_trusted_certificate directive. The FFI symbol is looked up softly so loading lua-resty-core against an older lua-nginx-module that lacks the new symbol still works; the method is simply not attached to the cosocket metatable in that case. Only the http subsystem is wired up, matching the FFI surface in ngx_http_lua_module. Stream cosockets are unchanged. Requires lua-nginx-module change "feature: support custom trusted CA store for cosocket TLS handshake". Signed-off-by: Walker Zhao <walker.zhao@konghq.com>
1 parent 9f27616 commit ad5ac4e

3 files changed

Lines changed: 362 additions & 0 deletions

File tree

README.markdown

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,7 @@ in the current request before you reusing the `ctx` table in some other place.
309309

310310
* [socket.setoption](https://github.com/openresty/lua-nginx-module#tcpsocksetoption)
311311
* [socket.setclientcert](https://github.com/openresty/lua-nginx-module#tcpsocksetclientcert)
312+
* [socket.settrustedstore](https://github.com/openresty/lua-nginx-module#tcpsocksettrustedstore)
312313
* [socket.sslhandshake](https://github.com/openresty/lua-nginx-module#tcpsocksslhandshake)
313314

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

lib/resty/core/socket.lua

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,10 @@ int
8585
ngx_http_lua_ffi_socket_tcp_get_ssl_ctx(ngx_http_request_t *r,
8686
ngx_http_lua_socket_tcp_upstream_t *u, void **pctx,
8787
char **errmsg);
88+
89+
int
90+
ngx_http_lua_ffi_socket_tcp_settrustedstore(ngx_http_request_t *r,
91+
ngx_http_lua_socket_tcp_upstream_t *u, void *store, char **errmsg);
8892
]]
8993

9094
ngx_lua_ffi_socket_tcp_getoption = C.ngx_http_lua_ffi_socket_tcp_getoption
@@ -155,6 +159,7 @@ local SOCKET_CTX_INDEX = 1
155159
local SOCKET_CLIENT_CERT_INDEX = 6
156160
local SOCKET_CLIENT_PKEY_INDEX = 7
157161
local SOCKET_IP_TRANSPARENT_INDEX = 9
162+
local SOCKET_TRUSTED_STORE_INDEX = 10
158163

159164

160165
local function get_tcp_socket(cosocket)
@@ -327,6 +332,48 @@ local function setclientcert(cosocket, cert, pkey)
327332
end
328333

329334

335+
local ngx_lua_ffi_socket_tcp_settrustedstore
336+
if pcall(function()
337+
return C.ngx_http_lua_ffi_socket_tcp_settrustedstore
338+
end) then
339+
ngx_lua_ffi_socket_tcp_settrustedstore =
340+
C.ngx_http_lua_ffi_socket_tcp_settrustedstore
341+
end
342+
343+
344+
local NULL_STORE = ffi_new("void *", nil)
345+
346+
347+
local function settrustedstore(cosocket, store)
348+
if not ngx_lua_ffi_socket_tcp_settrustedstore then
349+
return nil, "tcpsock:settrustedstore is not supported by " ..
350+
"the current lua-nginx-module"
351+
end
352+
353+
if store ~= nil and type(store) ~= "cdata" then
354+
return nil, "bad store arg: cdata expected, got " .. type(store)
355+
end
356+
357+
local r = get_request()
358+
if not r then
359+
error("no request found", 2)
360+
end
361+
362+
local u = get_tcp_socket(cosocket)
363+
364+
local rc = ngx_lua_ffi_socket_tcp_settrustedstore(r, u,
365+
store or NULL_STORE,
366+
errmsg)
367+
if rc ~= FFI_OK then
368+
return nil, ffi_str(errmsg[0])
369+
end
370+
371+
cosocket[SOCKET_TRUSTED_STORE_INDEX] = store
372+
373+
return true
374+
end
375+
376+
330377
local function sslhandshake(cosocket, reused_session, server_name, ssl_verify,
331378
send_status_req, ...)
332379

@@ -443,6 +490,9 @@ do
443490
method_table.getoption = getoption
444491
method_table.setoption = setoption
445492
method_table.setclientcert = setclientcert
493+
if ngx_lua_ffi_socket_tcp_settrustedstore then
494+
method_table.settrustedstore = settrustedstore
495+
end
446496
method_table.sslhandshake = sslhandshake
447497
method_table.getfd = getfd
448498
method_table.getoption = getoption

t/socket-tcp-settrustedstore.t

Lines changed: 311 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,311 @@
1+
# vim:set ft= ts=4 sw=4 et fdm=marker:
2+
3+
use lib '.';
4+
use t::TestCore;
5+
6+
repeat_each(2);
7+
8+
my $NginxBinary = $ENV{'TEST_NGINX_BINARY'} || 'nginx';
9+
my $openssl_version = eval { `$NginxBinary -V 2>&1` };
10+
11+
if ($openssl_version =~ m/built with OpenSSL (0\S*|1\.0\S*|1\.1\.0\S*)/) {
12+
plan(skip_all => "too old OpenSSL, need 1.1.1, was $1");
13+
} else {
14+
plan tests => repeat_each() * (blocks() * 5);
15+
}
16+
17+
no_long_string();
18+
#no_diff();
19+
20+
env_to_nginx("PATH=" . $ENV{'PATH'});
21+
$ENV{TEST_NGINX_LUA_PACKAGE_PATH} = "$t::TestCore::lua_package_path";
22+
$ENV{TEST_NGINX_HTML_DIR} ||= html_dir();
23+
24+
# An http_config that:
25+
# 1. boots resty.core in init_by_lua_block (same as t::TestCore::HttpConfig);
26+
# 2. cdef's just enough OpenSSL to build an X509_STORE from a PEM blob and
27+
# exposes load_store_from_pem() as a global;
28+
# 3. stands up a TLS server on a unix socket presenting mtls_server.crt
29+
# (signed by mtls_ca) so the test cosocket has something to handshake
30+
# against.
31+
our $TLSHttpConfig = <<_EOC_;
32+
lua_package_path '$t::TestCore::lua_package_path';
33+
34+
init_by_lua_block {
35+
$t::TestCore::init_by_lua_block
36+
37+
local ffi = require "ffi"
38+
ffi.cdef[[
39+
typedef struct x509_store_st X509_STORE;
40+
typedef struct x509_st X509;
41+
typedef struct bio_st BIO;
42+
typedef struct bio_method_st BIO_METHOD;
43+
44+
X509_STORE *X509_STORE_new(void);
45+
int X509_STORE_add_cert(X509_STORE *ctx, X509 *x);
46+
void X509_STORE_free(X509_STORE *v);
47+
48+
BIO_METHOD *BIO_s_mem(void);
49+
BIO *BIO_new(BIO_METHOD *type);
50+
int BIO_write(BIO *b, const void *buf, int len);
51+
void BIO_free(BIO *a);
52+
X509 *PEM_read_bio_X509(BIO *bp, X509 **x, void *cb, void *u);
53+
void X509_free(X509 *a);
54+
]]
55+
56+
function _G.load_store_from_pem(pem)
57+
local C = ffi.C
58+
local bio = C.BIO_new(C.BIO_s_mem())
59+
if bio == nil then return nil, "BIO_new failed" end
60+
if C.BIO_write(bio, pem, #pem) <= 0 then
61+
C.BIO_free(bio)
62+
return nil, "BIO_write failed"
63+
end
64+
local x509 = C.PEM_read_bio_X509(bio, nil, nil, nil)
65+
C.BIO_free(bio)
66+
if x509 == nil then return nil, "PEM_read_bio_X509 failed" end
67+
local store = C.X509_STORE_new()
68+
if store == nil then
69+
C.X509_free(x509)
70+
return nil, "X509_STORE_new failed"
71+
end
72+
if C.X509_STORE_add_cert(store, x509) ~= 1 then
73+
C.X509_free(x509)
74+
C.X509_STORE_free(store)
75+
return nil, "X509_STORE_add_cert failed"
76+
end
77+
C.X509_free(x509)
78+
return ffi.gc(store, C.X509_STORE_free)
79+
end
80+
}
81+
82+
server {
83+
listen unix:\$TEST_NGINX_HTML_DIR/tls.sock ssl;
84+
ssl_certificate ../../cert/mtls_server.crt;
85+
ssl_certificate_key ../../cert/mtls_server.key;
86+
server_tokens off;
87+
88+
location / {
89+
content_by_lua_block {
90+
ngx.say("hello, ", ngx.var.ssl_protocol)
91+
}
92+
}
93+
}
94+
_EOC_
95+
96+
run_tests();
97+
98+
__DATA__
99+
100+
=== TEST 1: handshake succeeds with a custom X509 trusted store
101+
--- http_config eval: $::TLSHttpConfig
102+
--- config
103+
lua_ssl_verify_depth 2;
104+
105+
location /t {
106+
content_by_lua_block {
107+
local f = assert(io.open("t/cert/mtls_ca.crt", "r"))
108+
local pem = f:read("*a")
109+
f:close()
110+
111+
local store, err = load_store_from_pem(pem)
112+
if not store then
113+
ngx.say("failed to load store: ", err)
114+
return
115+
end
116+
117+
local sock = ngx.socket.tcp()
118+
sock:settimeout(3000)
119+
120+
local ok, err = sock:connect("unix:$TEST_NGINX_HTML_DIR/tls.sock")
121+
if not ok then
122+
ngx.say("failed to connect: ", err)
123+
return
124+
end
125+
126+
local ok, err = sock:settrustedstore(store)
127+
if not ok then
128+
ngx.say("failed to settrustedstore: ", err)
129+
return
130+
end
131+
132+
local sess, err = sock:sslhandshake(nil, "example.com", true)
133+
if not sess then
134+
ngx.say("failed to do SSL handshake: ", err)
135+
return
136+
end
137+
138+
local req = "GET / HTTP/1.0\r\nHost: example.com\r\nConnection: close\r\n\r\n"
139+
local bytes, err = sock:send(req)
140+
if not bytes then
141+
ngx.say("failed to send: ", err)
142+
return
143+
end
144+
145+
local line, err = sock:receive("*l")
146+
if not line then
147+
ngx.say("failed to receive: ", err)
148+
return
149+
end
150+
151+
ngx.say("received: ", line)
152+
sock:close()
153+
}
154+
}
155+
--- request
156+
GET /t
157+
--- response_body_like
158+
^received: HTTP/1\.0 200 OK
159+
--- no_error_log
160+
[error]
161+
[alert]
162+
[crit]
163+
164+
165+
166+
=== TEST 2: handshake fails when the trusted store has the wrong CA
167+
--- http_config eval: $::TLSHttpConfig
168+
--- config
169+
location /t {
170+
content_by_lua_block {
171+
local f = assert(io.open("t/cert/test.crt", "r"))
172+
local pem = f:read("*a")
173+
f:close()
174+
175+
local store, err = load_store_from_pem(pem)
176+
if not store then
177+
ngx.say("failed to load store: ", err)
178+
return
179+
end
180+
181+
local sock = ngx.socket.tcp()
182+
sock:settimeout(3000)
183+
assert(sock:connect("unix:$TEST_NGINX_HTML_DIR/tls.sock"))
184+
185+
local ok, err = sock:settrustedstore(store)
186+
if not ok then
187+
ngx.say("failed to settrustedstore: ", err)
188+
return
189+
end
190+
191+
local sess, err = sock:sslhandshake(nil, "example.com", true)
192+
if sess then
193+
ngx.say("unexpected success")
194+
else
195+
ngx.say("handshake failed: ", err)
196+
end
197+
198+
sock:close()
199+
}
200+
}
201+
--- request
202+
GET /t
203+
--- response_body_like
204+
^handshake failed: .*certificate verify
205+
--- error_log
206+
lua ssl certificate verify error
207+
--- no_error_log
208+
[alert]
209+
[crit]
210+
211+
212+
213+
=== TEST 3: bad arg type is rejected before any FFI / network work
214+
--- http_config eval: $::TLSHttpConfig
215+
--- config
216+
location /t {
217+
content_by_lua_block {
218+
local sock = ngx.socket.tcp()
219+
sock:settimeout(3000)
220+
assert(sock:connect("unix:$TEST_NGINX_HTML_DIR/tls.sock"))
221+
222+
local ok, err = sock:settrustedstore("not cdata")
223+
ngx.say("settrustedstore: ", ok, " ", err)
224+
225+
sock:close()
226+
}
227+
}
228+
--- request
229+
GET /t
230+
--- response_body
231+
settrustedstore: nil bad store arg: cdata expected, got string
232+
--- no_error_log
233+
[error]
234+
[alert]
235+
[crit]
236+
237+
238+
239+
=== TEST 4: settrustedstore on a closed socket returns "closed"
240+
--- http_config eval: $::TLSHttpConfig
241+
--- config
242+
location /t {
243+
content_by_lua_block {
244+
local f = assert(io.open("t/cert/mtls_ca.crt", "r"))
245+
local pem = f:read("*a")
246+
f:close()
247+
248+
local store = assert(load_store_from_pem(pem))
249+
250+
local sock = ngx.socket.tcp()
251+
sock:settimeout(3000)
252+
assert(sock:connect("unix:$TEST_NGINX_HTML_DIR/tls.sock"))
253+
assert(sock:close())
254+
255+
local ok, err = sock:settrustedstore(store)
256+
ngx.say("settrustedstore: ", ok, " ", err)
257+
}
258+
}
259+
--- request
260+
GET /t
261+
--- response_body
262+
settrustedstore: nil closed
263+
--- no_error_log
264+
[error]
265+
[alert]
266+
[crit]
267+
268+
269+
270+
=== TEST 5: passing nil clears the trusted store on both sides
271+
--- http_config eval: $::TLSHttpConfig
272+
--- config
273+
lua_ssl_trusted_certificate ../../cert/mtls_ca.crt;
274+
lua_ssl_verify_depth 2;
275+
276+
location /t {
277+
content_by_lua_block {
278+
-- First set a wrong CA, then clear it. The handshake should
279+
-- then succeed via lua_ssl_trusted_certificate, proving the
280+
-- C-side slot was cleared (not just the lua-side ref).
281+
local f = assert(io.open("t/cert/test.crt", "r"))
282+
local wrong_pem = f:read("*a")
283+
f:close()
284+
285+
local wrong_store = assert(load_store_from_pem(wrong_pem))
286+
287+
local sock = ngx.socket.tcp()
288+
sock:settimeout(3000)
289+
assert(sock:connect("unix:$TEST_NGINX_HTML_DIR/tls.sock"))
290+
291+
assert(sock:settrustedstore(wrong_store))
292+
assert(sock:settrustedstore(nil))
293+
294+
local sess, err = sock:sslhandshake(nil, "example.com", true)
295+
if not sess then
296+
ngx.say("handshake failed: ", err)
297+
return
298+
end
299+
300+
ngx.say("handshake ok")
301+
sock:close()
302+
}
303+
}
304+
--- request
305+
GET /t
306+
--- response_body
307+
handshake ok
308+
--- no_error_log
309+
[error]
310+
[alert]
311+
[crit]

0 commit comments

Comments
 (0)