feat: support client certificate verification (#4034)

Signed-off-by: spacewander <spacewanderlzx@gmail.com>
This commit is contained in:
罗泽轩 2021-04-22 22:00:02 +08:00 committed by GitHub
parent 93a9feb9b5
commit 544ab52a40
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 376 additions and 1 deletions

View File

@ -70,6 +70,17 @@ local function check_conf(id, conf, need_id)
end
end
if conf.client then
if not apisix_ssl.support_client_verification() then
return nil, {error_msg = "client tls verify unsupported"}
end
local ok, err = apisix_ssl.validate(conf.client.ca, nil)
if not ok then
return nil, {error_msg = "failed to validate client_cert: " .. err}
end
end
return need_id and id or true
end

View File

@ -281,6 +281,19 @@ end
function _M.http_access_phase()
local ngx_ctx = ngx.ctx
if ngx_ctx.api_ctx and ngx_ctx.api_ctx.ssl_client_verified then
local res = ngx_var.ssl_client_verify
if res ~= "SUCCESS" then
if res == "NONE" then
core.log.error("client certificate was not present")
else
core.log.error("clent certificate verification is not passed: ", res)
end
return core.response.exit(400)
end
end
-- always fetch table from the table pool, we don't need a reused api_ctx
local api_ctx = core.tablepool.fetch("api_ctx", 0, 32)
ngx_ctx.api_ctx = api_ctx

View File

@ -642,6 +642,18 @@ _M.ssl = {
type = "array",
items = private_key_schema,
},
client = {
type = "object",
properties = {
ca = certificate_scheme,
depth = {
type = "integer",
minimum = 0,
default = 1,
},
},
required = {"ca"},
},
exptime = {
type = "integer",
minimum = 1588262400, -- 2020/5/1 0:0:0

View File

@ -101,6 +101,11 @@ function _M.validate(cert, key)
return nil, "failed to parse cert: " .. err
end
if key == nil then
-- sometimes we only need to validate the cert
return true
end
key = aes_decrypt_pkey(key)
if not key then
return nil, "failed to decrypt previous encrypted key"
@ -152,4 +157,9 @@ function _M.fetch_pkey(sni, pkey)
end
function _M.support_client_verification()
return ngx_ssl.verify_client ~= nil
end
return _M

View File

@ -20,7 +20,7 @@ local core = require("apisix.core")
local apisix_ssl = require("apisix.ssl")
local ngx_ssl = require("ngx.ssl")
local config_util = require("apisix.core.config_util")
local ipairs = ipairs
local ipairs = ipairs
local type = type
local error = error
local str_find = core.string.find
@ -29,6 +29,7 @@ local ssl_certificates
local radixtree_router
local radixtree_router_ver
local _M = {
version = 0.1,
server_name = ngx_ssl.server_name,
@ -194,6 +195,24 @@ function _M.match_and_set(api_ctx)
end
end
if matched_ssl.value.client then
local ca_cert = matched_ssl.value.client.ca
local depth = matched_ssl.value.client.depth
if apisix_ssl.support_client_verification() then
local parsed_cert, err = apisix_ssl.fetch_cert(sni, ca_cert)
if not parsed_cert then
return false, "failed to parse client cert: " .. err
end
local ok, err = ngx_ssl.verify_client(parsed_cert, depth)
if not ok then
return false, err
end
api_ctx.ssl_client_verified = true
end
end
return true
end

View File

@ -784,6 +784,8 @@ Return response from etcd currently.
| key | True | Private key | https private key | |
| certs | False | An array of certificate | when you need to configure multiple certificate for the same domain, you can pass extra https certificates (excluding the one given as cert) in this field | |
| keys | False | An array of private key | https private keys. The keys should be paired with certs above | |
| client.ca | False | Certificate| set the CA certificate which will use to verify client. This feature requires OpenResty 1.19+. | |
| client.depth | False | Certificate| set the verification depth in the client certificates chain, default to 1. This feature requires OpenResty 1.19+. | |
| snis | True | Match Rules | a non-empty arrays of https SNI | |
| labels | False | Match Rules | Key/value pairs to specify attributes | {"version":"v2","build":"16","env":"production"} |
| create_time | False | Auxiliary | epoch timestamp in second, will be created automatically if missing | 1602883670 |

View File

@ -781,6 +781,8 @@ $ curl http://127.0.0.1:9080/get
| key | 必需 | 私钥 | https 证书私钥 | |
| certs | 可选 | 证书字符串数组 | 当你想给同一个域名配置多个证书时,除了第一个证书需要通过 cert 传递外,剩下的证书可以通过该参数传递上来 | |
| keys | 可选 | 私钥字符串数组 | certs 对应的证书私钥,注意要跟 certs 一一对应 | |
| client.ca | 可选 | 证书| 设置将用于客户端证书校验的 CA 证书。该特性需要 OpenResty 1.19+ | |
| client.depth | 可选 | 辅助| 设置客户端证书校验的深度,默认为 1。该特性需要 OpenResty 1.19+ | |
| snis | 必需 | 匹配规则 | 非空数组形式,可以匹配多个 SNI | |
| labels | 可选 | 匹配规则 | 标识附加属性的键值对 | {"version":"v2","build":"16","env":"production"} |
| create_time | 可选 | 辅助 | 单位为秒的 epoch 时间戳,如果不指定则自动创建 | 1602883670 |

306
t/node/client-mtls.t vendored Normal file
View File

@ -0,0 +1,306 @@
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
use t::APISIX;
my $nginx_binary = $ENV{'TEST_NGINX_BINARY'} || 'nginx';
my $version = eval { `$nginx_binary -V 2>&1` };
if ($version !~ m/\/apisix-nginx-module/) {
plan(skip_all => "apisix-nginx-module not installed");
} else {
plan('no_plan');
}
repeat_each(1);
log_level('info');
no_root_location();
no_shuffle();
add_block_preprocessor(sub {
my ($block) = @_;
if ((!defined $block->error_log) && (!defined $block->no_error_log)) {
$block->set_value("no_error_log", "[error]");
}
});
run_tests();
__DATA__
=== TEST 1: bad client certificate
--- config
location /t {
content_by_lua_block {
local t = require("lib.test_admin")
local json = require("toolkit.json")
local ssl_cert = t.read_file("t/certs/mtls_client.crt")
local ssl_key = t.read_file("t/certs/mtls_client.key")
local data = {
cert = ssl_cert,
key = ssl_key,
sni = "test.com",
client = {
ca = ("test.com"):rep(128),
}
}
local code, body = t.test('/apisix/admin/ssl/1',
ngx.HTTP_PUT,
json.encode(data)
)
if code >= 300 then
ngx.status = code
end
ngx.print(body)
}
}
--- request
GET /t
--- error_code: 400
--- response_body
{"error_msg":"failed to validate client_cert: failed to parse cert: PEM_read_bio_X509_AUX() failed"}
=== TEST 2: missing client certificate
--- config
location /t {
content_by_lua_block {
local t = require("lib.test_admin")
local json = require("toolkit.json")
local ssl_cert = t.read_file("t/certs/mtls_client.crt")
local ssl_key = t.read_file("t/certs/mtls_client.key")
local data = {
cert = ssl_cert,
key = ssl_key,
sni = "test.com",
client = {
}
}
local code, body = t.test('/apisix/admin/ssl/1',
ngx.HTTP_PUT,
json.encode(data)
)
if code >= 300 then
ngx.status = code
end
ngx.print(body)
}
}
--- request
GET /t
--- error_code: 400
--- response_body
{"error_msg":"invalid configuration: property \"client\" validation failed: property \"ca\" is required"}
=== TEST 3: set verification
--- config
location /t {
content_by_lua_block {
local t = require("lib.test_admin")
local json = require("toolkit.json")
local ssl_ca_cert = t.read_file("t/certs/mtls_ca.crt")
local ssl_cert = t.read_file("t/certs/mtls_client.crt")
local ssl_key = t.read_file("t/certs/mtls_client.key")
local data = {
upstream = {
scheme = "https",
type = "roundrobin",
nodes = {
["127.0.0.1:1994"] = 1,
},
tls = {
client_cert = ssl_cert,
client_key = ssl_key,
}
},
plugins = {
["proxy-rewrite"] = {
uri = "/hello"
}
},
uri = "/mtls"
}
local code, body = t.test('/apisix/admin/routes/1',
ngx.HTTP_PUT,
json.encode(data)
)
if code >= 300 then
ngx.status = code
ngx.say(body)
return
end
local data = {
upstream = {
type = "roundrobin",
nodes = {
["127.0.0.1:1980"] = 1,
},
},
uri = "/hello"
}
assert(t.test('/apisix/admin/routes/2',
ngx.HTTP_PUT,
json.encode(data)
))
local data = {
cert = ssl_cert,
key = ssl_key,
sni = "localhost",
client = {
ca = ssl_ca_cert,
depth = 2,
}
}
local code, body = t.test('/apisix/admin/ssl/1',
ngx.HTTP_PUT,
json.encode(data)
)
if code >= 300 then
ngx.status = code
end
ngx.print(body)
}
}
--- request
GET /t
=== TEST 4: hit
--- request
GET /mtls
--- more_headers
Host: localhost
--- response_body
hello world
=== TEST 5: no client certificate
--- config
location /t {
content_by_lua_block {
local t = require("lib.test_admin")
local json = require("toolkit.json")
local data = {
upstream = {
scheme = "https",
type = "roundrobin",
nodes = {
["127.0.0.1:1994"] = 1,
},
},
plugins = {
["proxy-rewrite"] = {
uri = "/hello"
}
},
uri = "/mtls2"
}
local code, body = t.test('/apisix/admin/routes/1',
ngx.HTTP_PUT,
json.encode(data)
)
if code >= 300 then
ngx.status = code
ngx.say(body)
return
end
ngx.print(body)
}
}
--- request
GET /t
=== TEST 6: hit
--- request
GET /mtls2
--- more_headers
Host: localhost
--- error_code: 400
--- error_log
client certificate was not present
=== TEST 7: wrong client certificate
--- config
location /t {
content_by_lua_block {
local t = require("lib.test_admin")
local json = require("toolkit.json")
local ssl_cert = t.read_file("t/certs/apisix.crt")
local ssl_key = t.read_file("t/certs/apisix.key")
local data = {
upstream = {
scheme = "https",
type = "roundrobin",
nodes = {
["127.0.0.1:1994"] = 1,
},
tls = {
client_cert = ssl_cert,
client_key = ssl_key,
}
},
plugins = {
["proxy-rewrite"] = {
uri = "/hello"
}
},
uri = "/mtls3"
}
local code, body = t.test('/apisix/admin/routes/1',
ngx.HTTP_PUT,
json.encode(data)
)
if code >= 300 then
ngx.status = code
ngx.say(body)
return
end
ngx.print(body)
}
}
--- request
GET /t
=== TEST 8: hit
--- request
GET /mtls3
--- more_headers
Host: localhost
--- error_code: 400
--- error_log
clent certificate verification is not passed: FAILED:self signed certificate