This is an automated email from the ASF dual-hosted git repository. wenming pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/apisix.git
The following commit(s) were added to refs/heads/master by this push: new 2886b21 feature: support multiple certificates(RSA and ECC) for single domain (#2089) 2886b21 is described below commit 2886b2169b67a729bcd58807572e9c849464797e Author: Alex Zhang <zchao1...@gmail.com> AuthorDate: Mon Aug 24 09:23:06 2020 +0800 feature: support multiple certificates(RSA and ECC) for single domain (#2089) --- apisix/admin/ssl.lua | 6 ++ apisix/http/router/radixtree_sni.lua | 18 ++++- apisix/schema_def.lua | 16 +++++ conf/cert/apisix_ecc.crt | 13 ++++ conf/cert/apisix_ecc.key | 8 +++ doc/https.md | 17 ++++- doc/zh-cn/https.md | 10 +++ t/APISIX.pm | 6 ++ t/admin/ssl.t | 89 ++++++++++++++++++++++++ t/router/radixtree-sni.t | 131 +++++++++++++++++++++++++++++++++++ 10 files changed, 311 insertions(+), 3 deletions(-) diff --git a/apisix/admin/ssl.lua b/apisix/admin/ssl.lua index 6d9307d..f102f27 100644 --- a/apisix/admin/ssl.lua +++ b/apisix/admin/ssl.lua @@ -54,6 +54,12 @@ local function check_conf(id, conf, need_id) return nil, {error_msg = "invalid configuration: " .. err} end + local numcerts = conf.certs and #conf.certs or 0 + local numkeys = conf.keys and #conf.keys or 0 + if numcerts ~= numkeys then + return nil, {error_msg = "mismatched number of certs and keys"} + end + return need_id and id or true end diff --git a/apisix/http/router/radixtree_sni.lua b/apisix/http/router/radixtree_sni.lua index 4c7843b..0663336 100644 --- a/apisix/http/router/radixtree_sni.lua +++ b/apisix/http/router/radixtree_sni.lua @@ -115,8 +115,6 @@ local function set_pem_ssl_key(cert, pkey) return false, "no request found" end - ngx_ssl.clear_certs() - local parse_cert, err = ngx_ssl.parse_pem_cert(cert) if parse_cert then local ok, err = ngx_ssl.set_cert(parse_cert) @@ -195,11 +193,27 @@ function _M.match_and_set(api_ctx) local matched_ssl = api_ctx.matched_ssl core.log.info("debug - matched: ", core.json.delay_encode(matched_ssl, true)) + + ngx_ssl.clear_certs() + ok, err = set_pem_ssl_key(matched_ssl.value.cert, matched_ssl.value.key) if not ok then return false, err end + -- multiple certificates support. + if matched_ssl.value.certs then + for i = 1, #matched_ssl.value.certs do + local cert = matched_ssl.value.certs[i] + local key = matched_ssl.value.keys[i] + + ok, err = set_pem_ssl_key(cert, key) + if not ok then + return false, err + end + end + end + return true end diff --git a/apisix/schema_def.lua b/apisix/schema_def.lua index 9e4604f..222938b 100644 --- a/apisix/schema_def.lua +++ b/apisix/schema_def.lua @@ -516,6 +516,22 @@ _M.ssl = { pattern = [[^\*?[0-9a-zA-Z-.]+$]], } }, + certs = { + type = "array", + items = { + type = "string", + minLength = 128, + maxLength = 64*1024, + } + }, + keys = { + type = "array", + items = { + type = "string", + minLength = 128, + maxLength = 64*1024, + } + }, exptime = { type = "integer", minimum = 1588262400, -- 2020/5/1 0:0:0 diff --git a/conf/cert/apisix_ecc.crt b/conf/cert/apisix_ecc.crt new file mode 100644 index 0000000..b40240e --- /dev/null +++ b/conf/cert/apisix_ecc.crt @@ -0,0 +1,13 @@ +-----BEGIN CERTIFICATE----- +MIIB8jCCAZmgAwIBAgIJALwxr+GMOgSKMAoGCCqGSM49BAMCMFYxCzAJBgNVBAYT +AkNOMRIwEAYDVQQIDAlHdWFuZ0RvbmcxDzANBgNVBAcMBlpodUhhaTEPMA0GA1UE +CgwGaXJlc3R5MREwDwYDVQQDDAh0ZXN0LmNvbTAeFw0yMDA4MTgwODI0MzdaFw0y +MTA4MTgwODI0MzdaMFYxCzAJBgNVBAYTAkNOMRIwEAYDVQQIDAlHdWFuZ0Rvbmcx +DzANBgNVBAcMBlpodUhhaTEPMA0GA1UECgwGaXJlc3R5MREwDwYDVQQDDAh0ZXN0 +LmNvbTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABEJACTxb5qYd4v9VaNKlv2fe +XlZSTDYe+0fZwT4l9sifmPzmpwjiVTB2wLiCYYzy+BPrb29r5ubgtXIflsWKRBKj +UDBOMB0GA1UdDgQWBBQPXxOYAHfboUjsoo1xm6/GJ1qHijAfBgNVHSMEGDAWgBQP +XxOYAHfboUjsoo1xm6/GJ1qHijAMBgNVHRMEBTADAQH/MAoGCCqGSM49BAMCA0cA +MEQCICQ70LiJ+Z2lv9ZF+FQL+VEdVQ938rz6RGXBmnl2oEvkAiBY2eeTl//JanNX +GsSV104WrpHjcBjcY24jb11Y1H3R9g== +-----END CERTIFICATE----- diff --git a/conf/cert/apisix_ecc.key b/conf/cert/apisix_ecc.key new file mode 100644 index 0000000..984f3ea --- /dev/null +++ b/conf/cert/apisix_ecc.key @@ -0,0 +1,8 @@ +-----BEGIN EC PARAMETERS----- +BggqhkjOPQMBBw== +-----END EC PARAMETERS----- +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIBlQqVD3bK6CQT5puOOngrb50+3K66MKJdhtpWQoUw2poAoGCCqGSM49 +AwEHoUQDQgAEQkAJPFvmph3i/1Vo0qW/Z95eVlJMNh77R9nBPiX2yJ+Y/OanCOJV +MHbAuIJhjPL4E+tvb2vm5uC1ch+WxYpEEg== +-----END EC PRIVATE KEY----- diff --git a/doc/https.md b/doc/https.md index c2091a7..7e4bd7a 100644 --- a/doc/https.md +++ b/doc/https.md @@ -21,7 +21,7 @@ ### HTTPS -`APISIX` supports to load a specific SSL certificate by TLS extension Server Name Indication (SNI). +`APISIX` supports to load multiple SSL certificates by TLS extension Server Name Indication (SNI). ### Single SNI @@ -103,3 +103,18 @@ curl --resolve 'www.test.com:9443:127.0.0.1' https://www.test.com:9443/hello -v If your SSL certificate may contain more than one domain, like `www.test.com` and `mail.test.com`, then you can more ssl object for each domain, that is a most simple way. + +### multiple certificates for a single domain + +If you want to configure multiple certificate for a single domain, for +instance, supporting both the +[ECC](https://en.wikipedia.org/wiki/Elliptic-curve_cryptography) +and RSA key-exchange algorithm, then just configure the extra certificates (the +first certificate and private key should be still put in `cert` and `key`) and +private keys by `certs` and `keys`. + +* `certs`: PEM-encoded certificate array. +* `keys`: PEM-encoded private key array. + +`APISIX` will pair certificate and private key with the same indice as a SSL key +pair. So the length of `certs` and `keys` must be same. diff --git a/doc/zh-cn/https.md b/doc/zh-cn/https.md index 33ce14c..d034f66 100644 --- a/doc/zh-cn/https.md +++ b/doc/zh-cn/https.md @@ -104,3 +104,13 @@ curl --resolve 'www.test.com:9443:127.0.0.1' https://www.test.com:9443/hello -v 如果一个 SSL 证书包含多个独立域名,比如`www.test.com`和`mail.test.com`,通配符方式又会导致匹配不严谨。 所以针对不同域名,设置不同 SSL 证书对象即可。 + +### 单域名多证书的情况 + +如果你期望为一个域名配置多张证书,例如以此来同时支持使用 ECC 和 RSA +的密钥交换算法,那么你可以将额外的证书和私钥(第一张证书和其私钥依然使用 `cert` 和 `key`)配置在 `certs` 和 `keys` 中。 + +* `certs`:PEM 格式的 SSL 证书列表 +* `keys`:PEM 格式的 SSL 证书私钥列表 + +`APISIX` 会将相同下标的证书和私钥配对使用,因此 `certs` 和 `keys` 列表的长度必须一致。 diff --git a/t/APISIX.pm b/t/APISIX.pm index c12c240..116ac98 100644 --- a/t/APISIX.pm +++ b/t/APISIX.pm @@ -73,6 +73,8 @@ my $default_yaml_config = read_file("conf/config-default.yaml"); my $user_yaml_config = read_file("conf/config.yaml"); my $ssl_crt = read_file("conf/cert/apisix.crt"); my $ssl_key = read_file("conf/cert/apisix.key"); +my $ssl_ecc_crt = read_file("conf/cert/apisix_ecc.crt"); +my $ssl_ecc_key = read_file("conf/cert/apisix_ecc.key"); my $test2_crt = read_file("conf/cert/test2.crt"); my $test2_key = read_file("conf/cert/test2.key"); $user_yaml_config = <<_EOC_; @@ -438,6 +440,10 @@ $yaml_config $ssl_crt >>> ../conf/cert/apisix.key $ssl_key +>>> ../conf/cert/apisix_ecc.crt +$ssl_ecc_crt +>>> ../conf/cert/apisix_ecc.key +$ssl_ecc_key >>> ../conf/cert/test2.crt $test2_crt >>> ../conf/cert/test2.key diff --git a/t/admin/ssl.t b/t/admin/ssl.t index e93ca19..647c962 100644 --- a/t/admin/ssl.t +++ b/t/admin/ssl.t @@ -441,3 +441,92 @@ GET /t --- error_code: 400 --- no_error_log [error] + + + +=== TEST 13: set ssl with multicerts(id: 1) +--- config + location /t { + content_by_lua_block { + local core = require("apisix.core") + local t = require("lib.test_admin") + + local ssl_cert = t.read_file("conf/cert/apisix.crt") + local ssl_key = t.read_file("conf/cert/apisix.key") + local ssl_ecc_cert = t.read_file("conf/cert/apisix_ecc.crt") + local ssl_ecc_key = t.read_file("conf/cert/apisix_ecc.key") + local data = { + cert = ssl_cert, + key = ssl_key, + sni = "test.com", + certs = {ssl_ecc_cert}, + keys = {ssl_ecc_key} + } + + local code, body = t.test('/apisix/admin/ssl/1', + ngx.HTTP_PUT, + core.json.encode(data), + [[{ + "node": { + "value": { + "sni": "test.com" + }, + "key": "/apisix/ssl/1" + }, + "action": "set" + }]] + ) + + ngx.status = code + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 14: mismatched certs and keys +--- config + location /t { + content_by_lua_block { + local core = require("apisix.core") + local t = require("lib.test_admin") + + local ssl_ecc_cert = t.read_file("conf/cert/apisix_ecc.crt") + + local data = { + sni = "test.com", + certs = { ssl_ecc_cert }, + keys = {}, + } + + local code, body = t.test('/apisix/admin/ssl/1', + ngx.HTTP_PUT, + core.json.encode(data), + [[{ + "node": { + "value": { + "sni": "test.com" + }, + "key": "/apisix/ssl/1" + }, + "action": "set" + }]] + ) + + ngx.status = code + ngx.print(body) + } + } +--- request +GET /t +--- error_code: 400 +--- response_body +{"error_msg":"invalid configuration: value should match only one schema, but matches none"} +--- no_error_log +[error] diff --git a/t/router/radixtree-sni.t b/t/router/radixtree-sni.t index 80e1a5b..7a4eec1 100644 --- a/t/router/radixtree-sni.t +++ b/t/router/radixtree-sni.t @@ -940,3 +940,134 @@ connected: 1 failed to do SSL handshake: handshake failed --- error_log decrypt ssl key failed. + + + +=== TEST 21 set ssl with multiple certificates. +--- config +location /t { + content_by_lua_block { + local core = require("apisix.core") + local t = require("lib.test_admin") + + local ssl_cert = t.read_file("conf/cert/apisix.crt") + local ssl_key = t.read_file("conf/cert/apisix.key") + local ssl_ecc_cert = t.read_file("conf/cert/apisix_ecc.crt") + local ssl_ecc_key = t.read_file("conf/cert/apisix_ecc.key") + + local data = { + cert = ssl_cert, + key = ssl_key, + certs = { ssl_ecc_cert }, + keys = { ssl_ecc_key }, + sni = "test.com", + } + + local code, body = t.test('/apisix/admin/ssl/1', + ngx.HTTP_PUT, + core.json.encode(data), + [[{ + "node": { + "value": { + "sni": "test.com" + }, + "key": "/apisix/ssl/1" + }, + "action": "set" + }]] + ) + ngx.status = code + ngx.say(body) + } +} +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 22: client request using ECC certificate +--- config +listen unix:$TEST_NGINX_HTML_DIR/nginx.sock ssl; +location /t { + lua_ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384; + content_by_lua_block { + -- etcd sync + + ngx.sleep(0.2) + + do + local sock = ngx.socket.tcp() + + sock:settimeout(2000) + + local ok, err = sock:connect("unix:$TEST_NGINX_HTML_DIR/nginx.sock") + if not ok then + ngx.say("failed to connect: ", err) + return + end + + ngx.say("connected: ", ok) + + local sess, err = sock:sslhandshake(nil, "test.com", false) + 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: userdata + + + +=== TEST 23: client request using RSA certificate +--- config +listen unix:$TEST_NGINX_HTML_DIR/nginx.sock ssl; + +location /t { + lua_ssl_ciphers ECDHE-RSA-AES256-SHA384; + content_by_lua_block { + -- etcd sync + + ngx.sleep(0.2) + + do + local sock = ngx.socket.tcp() + + sock:settimeout(2000) + + local ok, err = sock:connect("unix:$TEST_NGINX_HTML_DIR/nginx.sock") + if not ok then + ngx.say("failed to connect: ", err) + return + end + + ngx.say("connected: ", ok) + + local sess, err = sock:sslhandshake(nil, "test.com", false) + 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: userdata