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

Reply via email to