Skip to content

Commit 28191b0

Browse files
authored
Add experimental mTLS support (#263)
* Add experimental support for mTLS Note this requires patches to OpenResty not yet available in mainline. * Loosen test cases to expect either cdata or userdata * Skip mTLS test for CI * Improve connection error messages
1 parent 7552ac4 commit 28191b0

File tree

8 files changed

+563
-7
lines changed

8 files changed

+563
-7
lines changed

lib/resty/http_connect.lua

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
local ngx_re_gmatch = ngx.re.gmatch
22
local ngx_re_sub = ngx.re.sub
33
local ngx_re_find = ngx.re.find
4+
local ngx_log = ngx.log
5+
local ngx_WARN = ngx.WARN
46

57
--[[
68
A connection function that incorporates:
@@ -28,6 +30,19 @@ client:connect {
2830
ssl_verify = true, -- NOTE: defaults to true
2931
ctx = nil, -- NOTE: not supported
3032
33+
-- mTLS options (experimental!)
34+
--
35+
-- !!! IMPORTANT !!! These options require support for mTLS in cosockets,
36+
-- which is currently only available in the following unmerged PRs.
37+
--
38+
-- * https://github.com/openresty/lua-nginx-module/pull/1602
39+
-- * https://github.com/openresty/lua-resty-core/pull/278
40+
--
41+
-- The details of this feature may change. You have been warned!
42+
--
43+
ssl_client_cert = nil,
44+
ssl_client_priv_key = nil,
45+
3146
proxy_opts, -- proxy opts, defaults to global proxy options
3247
}
3348
]]
@@ -54,7 +69,8 @@ local function connect(self, options)
5469
end
5570

5671
-- ssl settings
57-
local ssl, ssl_reused_session, ssl_server_name, ssl_verify, ssl_send_status_req
72+
local ssl, ssl_reused_session, ssl_server_name
73+
local ssl_verify, ssl_send_status_req, ssl_client_cert, ssl_client_priv_key
5874
if request_scheme == "https" then
5975
ssl = true
6076
ssl_reused_session = options.ssl_reused_session
@@ -64,6 +80,8 @@ local function connect(self, options)
6480
if options.ssl_verify == false then
6581
ssl_verify = false
6682
end
83+
ssl_client_cert = options.ssl_client_cert
84+
ssl_client_priv_key = options.ssl_client_priv_key
6785
end
6886

6987
-- proxy related settings
@@ -138,7 +156,7 @@ local function connect(self, options)
138156
local proxy_uri_t
139157
proxy_uri_t, err = self:parse_uri(proxy_uri)
140158
if not proxy_uri_t then
141-
return nil, err
159+
return nil, "uri parse error: ", err
142160
end
143161

144162
local proxy_scheme = proxy_uri_t[1]
@@ -172,7 +190,9 @@ local function connect(self, options)
172190
-- proxy based connection
173191
ok, err = sock:connect(proxy_host, proxy_port, tcp_opts)
174192
if not ok then
175-
return nil, err
193+
return nil, "failed to connect to: " .. (proxy_host or "") ..
194+
":" .. (proxy_port or "") ..
195+
": ", err
176196
end
177197

178198
if ssl and sock:getreusedtimes() == 0 then
@@ -192,7 +212,7 @@ local function connect(self, options)
192212
})
193213

194214
if not res then
195-
return nil, err
215+
return nil, "failed to issue CONNECT to proxy:", err
196216
end
197217

198218
if res.status < 200 or res.status > 299 then
@@ -218,6 +238,21 @@ local function connect(self, options)
218238
local ssl_session
219239
-- Now do the ssl handshake
220240
if ssl and sock:getreusedtimes() == 0 then
241+
242+
-- Experimental mTLS support
243+
if ssl_client_cert and ssl_client_priv_key then
244+
if type(sock.setclientcert) ~= "function" then
245+
ngx_log(ngx_WARN, "cannot use SSL client cert and key without mTLS support")
246+
247+
else
248+
-- currently no return value
249+
ok, err = sock:setclientcert(ssl_client_cert, ssl_client_priv_key)
250+
if not ok then
251+
ngx_log(ngx_WARN, "could not set client certificate: ", err)
252+
end
253+
end
254+
end
255+
221256
ssl_session, err = sock:sslhandshake(ssl_reused_session, ssl_server_name, ssl_verify, ssl_send_status_req)
222257
if not ssl_session then
223258
self:close()

t/19-ssl_reused_session.t

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ __DATA__
5757
host = TEST_SERVER_SOCK,
5858
})
5959
60-
assert(type(session) == "userdata", "expected session to be userdata")
60+
assert(type(session) == "userdata" or type(session) == "cdata", "expected session to be userdata or cdata")
6161
assert(httpc:close())
6262
}
6363
}
@@ -113,7 +113,7 @@ GET /t
113113
host = TEST_SERVER_SOCK,
114114
})
115115
116-
assert(type(session) == "userdata", "expected session to be userdata")
116+
assert(type(session) == "userdata" or type(session) == "cdata", "expected session to be userdata or cdata")
117117
118118
local httpc2 = assert(require("resty.http").new())
119119
local ok, err, session2 = assert(httpc2:connect {
@@ -122,7 +122,7 @@ GET /t
122122
ssl_reused_session = session,
123123
})
124124
125-
assert(type(session2) == "userdata", "expected session2 to be userdata")
125+
assert(type(session2) == "userdata" or type(session2) == "cdata", "expected session2 to be userdata or cdata")
126126
127127
assert(httpc:close())
128128
assert(httpc2:close())

t/20-mtls.t

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
use Test::Nginx::Socket::Lua 'no_plan';
2+
3+
4+
#$ENV{TEST_NGINX_RESOLVER} ||= '8.8.8.8';
5+
#$ENV{TEST_COVERAGE} ||= 0;
6+
7+
log_level 'debug';
8+
9+
no_long_string();
10+
#no_diff();
11+
12+
sub read_file {
13+
my $infile = shift;
14+
open my $in, $infile
15+
or die "cannot open $infile for reading: $!";
16+
my $cert = do { local $/; <$in> };
17+
close $in;
18+
$cert;
19+
}
20+
21+
our $MTLSCA = read_file("t/cert/mtls_ca.crt");
22+
our $MTLSClient = read_file("t/cert/mtls_client.crt");
23+
our $MTLSClientKey = read_file("t/cert/mtls_client.key");
24+
our $TestCert = read_file("t/cert/test.crt");
25+
our $TestKey = read_file("t/cert/test.key");
26+
27+
our $HtmlDir = html_dir;
28+
29+
use Cwd qw(cwd);
30+
my $pwd = cwd();
31+
32+
our $mtls_http_config = <<"_EOC_";
33+
lua_package_path "$pwd/lib/?.lua;/usr/local/share/lua/5.1/?.lua;;";
34+
server {
35+
listen unix:$::HtmlDir/mtls.sock ssl;
36+
37+
ssl_certificate $::HtmlDir/test.crt;
38+
ssl_certificate_key $::HtmlDir/test.key;
39+
ssl_client_certificate $::HtmlDir/mtls_ca.crt;
40+
ssl_verify_client on;
41+
server_tokens off;
42+
server_name example.com;
43+
44+
location / {
45+
echo -n "hello, \$ssl_client_s_dn";
46+
}
47+
}
48+
_EOC_
49+
50+
our $mtls_user_files = <<"_EOC_";
51+
>>> mtls_ca.crt
52+
$::MTLSCA
53+
>>> mtls_client.key
54+
$::MTLSClientKey
55+
>>> mtls_client.crt
56+
$::MTLSClient
57+
>>> test.crt
58+
$::TestCert
59+
>>> test.key
60+
$::TestKey
61+
_EOC_
62+
63+
run_tests();
64+
65+
__DATA__
66+
67+
=== TEST 1: Connection fails during handshake without client cert and key
68+
--- http_config eval: $::mtls_http_config
69+
--- config eval
70+
"
71+
lua_ssl_trusted_certificate $::HtmlDir/test.crt;
72+
location /t {
73+
content_by_lua_block {
74+
local httpc = assert(require('resty.http').new())
75+
76+
local ok, err = httpc:connect {
77+
scheme = 'https',
78+
host = 'unix:$::HtmlDir/mtls.sock',
79+
}
80+
81+
if ok and not err then
82+
local res, err = assert(httpc:request {
83+
method = 'GET',
84+
path = '/',
85+
headers = {
86+
['Host'] = 'example.com',
87+
},
88+
})
89+
90+
ngx.status = res.status -- expect 400
91+
end
92+
93+
httpc:close()
94+
}
95+
}
96+
"
97+
--- user_files eval: $::mtls_user_files
98+
--- request
99+
GET /t
100+
--- error_code: 400
101+
--- no_error_log
102+
[error]
103+
[warn]
104+
105+
106+
=== TEST 2: Connection fails during handshake with not priv_key
107+
--- http_config eval: $::mtls_http_config
108+
--- SKIP
109+
--- config eval
110+
"
111+
lua_ssl_trusted_certificate $::HtmlDir/test.crt;
112+
location /t {
113+
content_by_lua_block {
114+
local f = assert(io.open('$::HtmlDir/mtls_client.crt'))
115+
local cert_data = f:read('*a')
116+
f:close()
117+
118+
local ssl = require('ngx.ssl')
119+
120+
local cert = assert(ssl.parse_pem_cert(cert_data))
121+
122+
local httpc = assert(require('resty.http').new())
123+
124+
local ok, err = httpc:connect {
125+
scheme = 'https',
126+
host = 'unix:$::HtmlDir/mtls.sock',
127+
ssl_client_cert = cert,
128+
ssl_client_priv_key = 'foo',
129+
}
130+
131+
if ok and not err then
132+
local res, err = assert(httpc:request {
133+
method = 'GET',
134+
path = '/',
135+
headers = {
136+
['Host'] = 'example.com',
137+
},
138+
})
139+
140+
ngx.say(res:read_body())
141+
end
142+
143+
httpc:close()
144+
}
145+
}
146+
"
147+
--- user_files eval: $::mtls_user_files
148+
--- request
149+
GET /t
150+
--- error_code: 200
151+
--- error_log
152+
could not set client certificate: bad client pkey type
153+
--- response_body_unlike: hello, CN=foo@example.com,O=OpenResty,ST=California,C=US
154+
155+
156+
=== TEST 3: Connection succeeds with client cert and key. SKIP'd for CI until feature is merged.
157+
--- SKIP
158+
--- http_config eval: $::mtls_http_config
159+
--- config eval
160+
"
161+
lua_ssl_trusted_certificate $::HtmlDir/test.crt;
162+
163+
location /t {
164+
content_by_lua_block {
165+
local f = assert(io.open('$::HtmlDir/mtls_client.crt'))
166+
local cert_data = f:read('*a')
167+
f:close()
168+
169+
f = assert(io.open('$::HtmlDir/mtls_client.key'))
170+
local key_data = f:read('*a')
171+
f:close()
172+
173+
local ssl = require('ngx.ssl')
174+
175+
local cert = assert(ssl.parse_pem_cert(cert_data))
176+
local key = assert(ssl.parse_pem_priv_key(key_data))
177+
178+
local httpc = assert(require('resty.http').new())
179+
180+
local ok, err = httpc:connect {
181+
scheme = 'https',
182+
host = 'unix:$::HtmlDir/mtls.sock',
183+
ssl_client_cert = cert,
184+
ssl_client_priv_key = key,
185+
}
186+
187+
if ok and not err then
188+
local res, err = assert(httpc:request {
189+
method = 'GET',
190+
path = '/',
191+
headers = {
192+
['Host'] = 'example.com',
193+
},
194+
})
195+
196+
ngx.say(res:read_body())
197+
end
198+
199+
httpc:close()
200+
}
201+
}
202+
"
203+
--- user_files eval: $::mtls_user_files
204+
--- request
205+
GET /t
206+
--- no_error_log
207+
[error]
208+
[warn]
209+
--- response_body
210+
hello, CN=foo@example.com,O=OpenResty,ST=California,C=US
211+

t/cert/mtls_ca.crt

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
-----BEGIN CERTIFICATE-----
2+
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
32+
79DprjVU8t7p
33+
-----END CERTIFICATE-----

0 commit comments

Comments
 (0)