Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: validate certificate & key #3085

Merged
merged 1 commit into from
Dec 23, 2020
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 15 additions & 29 deletions apisix/admin/ssl.lua
Original file line number Diff line number Diff line change
@@ -16,11 +16,9 @@
--
local core = require("apisix.core")
local utils = require("apisix.admin.utils")
local apisix_ssl = require("apisix.ssl")
local tostring = tostring
local aes = require "resty.aes"
local ngx_encode_base64 = ngx.encode_base64
local type = type
local assert = assert

local _M = {
version = 0.1,
@@ -54,37 +52,25 @@ local function check_conf(id, conf, need_id)
return nil, {error_msg = "invalid configuration: " .. err}
end

local ok, err = apisix_ssl.validate(conf.cert, conf.key)
if not ok then
return nil, {error_msg = 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


local function aes_encrypt(origin)
local local_conf = core.config.local_conf()
local iv
if local_conf and local_conf.apisix
and local_conf.apisix.ssl.key_encrypt_salt then
iv = local_conf.apisix.ssl.key_encrypt_salt
end
local aes_128_cbc_with_iv = (type(iv)=="string" and #iv == 16) and
assert(aes:new(iv, nil, aes.cipher(128, "cbc"), {iv=iv})) or nil

if aes_128_cbc_with_iv ~= nil and core.string.has_prefix(origin, "---") then
local encrypted = aes_128_cbc_with_iv:encrypt(origin)
if encrypted == nil then
core.log.error("failed to encrypt key[", origin, "] ")
return origin
for i = 1, numcerts do
local ok, err = apisix_ssl.validate(conf.certs[i], conf.keys[i])
if not ok then
return nil, {error_msg = "failed to handle cert-key pair[" .. i .. "]: " .. err}
end

return ngx_encode_base64(encrypted)
end

return origin
return need_id and id or true
end


@@ -95,11 +81,11 @@ function _M.put(id, conf)
end

-- encrypt private key
conf.key = aes_encrypt(conf.key)
conf.key = apisix_ssl.aes_encrypt_pkey(conf.key)

if conf.keys then
for i = 1, #conf.keys do
conf.keys[i] = aes_encrypt(conf.keys[i])
conf.keys[i] = apisix_ssl.aes_encrypt_pkey(conf.keys[i])
end
end

@@ -148,11 +134,11 @@ function _M.post(id, conf)
end

-- encrypt private key
conf.key = aes_encrypt(conf.key)
conf.key = apisix_ssl.aes_encrypt_pkey(conf.key)

if conf.keys then
for i = 1, #conf.keys do
conf.keys[i] = aes_encrypt(conf.keys[i])
conf.keys[i] = apisix_ssl.aes_encrypt_pkey(conf.keys[i])
end
end

111 changes: 111 additions & 0 deletions apisix/ssl.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
--
-- 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.
--
local core = require("apisix.core")
local ngx_ssl = require("ngx.ssl")
local ngx_encode_base64 = ngx.encode_base64
local ngx_decode_base64 = ngx.decode_base64
local aes = require "resty.aes"
local assert = assert
local type = type


local _M = {}


local _aes_128_cbc_with_iv = false
local function get_aes_128_cbc_with_iv()
if _aes_128_cbc_with_iv == false then
local local_conf = core.config.local_conf()
local iv = core.table.try_read_attr(local_conf, "apisix", "ssl", "key_encrypt_salt")
if type(iv) =="string" and #iv == 16 then
_aes_128_cbc_with_iv = assert(aes:new(iv, nil, aes.cipher(128, "cbc"), {iv = iv}))
else
_aes_128_cbc_with_iv = nil
end
end
return _aes_128_cbc_with_iv
end


function _M.aes_encrypt_pkey(origin)
local aes_128_cbc_with_iv = get_aes_128_cbc_with_iv()
if aes_128_cbc_with_iv ~= nil and core.string.has_prefix(origin, "---") then
local encrypted = aes_128_cbc_with_iv:encrypt(origin)
if encrypted == nil then
core.log.error("failed to encrypt key[", origin, "] ")
return origin
end

return ngx_encode_base64(encrypted)
end

return origin
end


local function decrypt_priv_pkey(iv, key)
local decoded_key = ngx_decode_base64(key)
if not decoded_key then
core.log.error("base64 decode ssl key failed and skipped. key[", key, "] ")
return nil
end

local decrypted = iv:decrypt(decoded_key)
if not decrypted then
core.log.error("decrypt ssl key failed and skipped. key[", key, "] ")
end

return decrypted
end


local function aes_decrypt_pkey(origin)
if core.string.has_prefix(origin, "---") then
return origin
end

local aes_128_cbc_with_iv = get_aes_128_cbc_with_iv()
if aes_128_cbc_with_iv ~= nil then
return decrypt_priv_pkey(aes_128_cbc_with_iv, origin)
end
return origin
end
_M.aes_decrypt_pkey = aes_decrypt_pkey


function _M.validate(cert, key)
local parsed_cert, err = ngx_ssl.parse_pem_cert(cert)
if not parsed_cert then
return nil, "failed to parse cert: " .. err
end

key = aes_decrypt_pkey(key)
if not key then
return nil, "failed to decrypt previous encrypted key"
end

local parsed_key, err = ngx_ssl.parse_pem_priv_key(key)
if not parsed_key then
return nil, "failed to parse key: " .. err
end

-- TODO: check if key & cert match
return true
end


return _M
58 changes: 11 additions & 47 deletions apisix/ssl/router/radixtree_sni.lua
Original file line number Diff line number Diff line change
@@ -17,16 +17,14 @@
local get_request = require("resty.core.base").get_request
local radixtree_new = require("resty.radixtree").new
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 type = type
local error = error
local str_find = core.string.find
local aes = require "resty.aes"
local assert = assert
local str_gsub = string.gsub
local ngx_decode_base64 = ngx.decode_base64
local ssl_certificates
local radixtree_router
local radixtree_router_ver
@@ -62,42 +60,12 @@ local function parse_pem_priv_key(sni, pkey)
end


local function decrypt_priv_pkey(iv, key)
if core.string.has_prefix(key, "---") then
return key
end

local decoded_key = ngx_decode_base64(key)
if not decoded_key then
core.log.error("base64 decode ssl key failed and skipped. key[", key, "] ")
return
end

local decrypted = iv:decrypt(decoded_key)
if not decrypted then
core.log.error("decrypt ssl key failed and skipped. key[", key, "] ")
end

return decrypted
end


local function create_router(ssl_items)
local ssl_items = ssl_items or {}

local route_items = core.table.new(#ssl_items, 0)
local idx = 0

local local_conf = core.config.local_conf()
local iv
if local_conf and local_conf.apisix
and local_conf.apisix.ssl
and local_conf.apisix.ssl.key_encrypt_salt then
iv = local_conf.apisix.ssl.key_encrypt_salt
end
local aes_128_cbc_with_iv = (type(iv)=="string" and #iv == 16) and
assert(aes:new(iv, nil, aes.cipher(128, "cbc"), {iv=iv})) or nil

for _, ssl in config_util.iterate_values(ssl_items) do
if ssl.value ~= nil and
(ssl.value.status == nil or ssl.value.status == 1) then -- compatible with old version
@@ -115,22 +83,18 @@ local function create_router(ssl_items)
end

-- decrypt private key
if aes_128_cbc_with_iv ~= nil then
if ssl.value.key then
local decrypted = decrypt_priv_pkey(aes_128_cbc_with_iv,
ssl.value.key)
if decrypted then
ssl.value.key = decrypted
end
if ssl.value.key then
local decrypted = apisix_ssl.aes_decrypt_pkey(ssl.value.key)
if decrypted then
ssl.value.key = decrypted
end
end

if ssl.value.keys then
for i = 1, #ssl.value.keys do
local decrypted = decrypt_priv_pkey(aes_128_cbc_with_iv,
ssl.value.keys[i])
if decrypted then
ssl.value.keys[i] = decrypted
end
if ssl.value.keys then
for i = 1, #ssl.value.keys do
local decrypted = apisix_ssl.aes_decrypt_pkey(ssl.value.keys[i])
if decrypted then
ssl.value.keys[i] = decrypted
end
end
end
140 changes: 140 additions & 0 deletions t/admin/ssl2.t
Original file line number Diff line number Diff line change
@@ -196,3 +196,143 @@ __DATA__
}
--- response_body
{"action":"delete","deleted":"1","key":"/apisix/ssl/1","node":{}}
=== TEST 6: bad cert
--- config
location /t {
content_by_lua_block {
local json = require("toolkit.json")
local t = require("lib.test_admin")
local ssl_key = t.read_file("t/certs/apisix.key")
local data = {cert = [[-----BEGIN CERTIFICATE-----
MIIEojCCAwqgAwIBAgIJAK253pMhgCkxMA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNV
BAYTAkNOMRIwEAYDVQQIDAlHdWFuZ0RvbmcxDzANBgNVBAcMBlpodUhhaTEPMA0G
U/OOcSRr39Kuis/JJ+DkgHYa/PWHZhnJQBxcqXXk1bJGw9BNbhM=
-----END CERTIFICATE-----
]], key = ssl_key, sni = "test.com"}
local code, message, res = t.test('/apisix/admin/ssl/1',
ngx.HTTP_PUT,
json.encode(data)
)
if code >= 300 then
ngx.status = code
ngx.print(message)
return
end
ngx.say(res)
}
}
--- error_code: 400
--- response_body
{"error_msg":"failed to parse cert: PEM_read_bio_X509_AUX() failed"}
=== TEST 7: bad key
--- config
location /t {
content_by_lua_block {
local json = require("toolkit.json")
local t = require("lib.test_admin")
local ssl_cert = t.read_file("t/certs/apisix.crt")
local data = {cert = ssl_cert, key = [[
-----BEGIN RSA PRIVATE KEY-----
MIIG5AIBAAKCAYEAyCM0rqJecvgnCfOw4fATotPwk5Ba0gC2YvIrO+gSbQkyxXF5
jhZB3W6BkWUWR4oNFLLSqcVbVDPitz/Mt46Mo8amuS6zTbQetGnBARzPLtmVhJfo
wzarryret/7GFW1/3cz+hTj9/d45i25zArr3Pocfpur5mfz3fJO8jg==
-----END RSA PRIVATE KEY-----]], sni = "test.com"}
local code, message, res = t.test('/apisix/admin/ssl/1',
ngx.HTTP_PUT,
json.encode(data)
)
if code >= 300 then
ngx.status = code
ngx.print(message)
return
end
ngx.say(res)
}
}
--- error_code: 400
--- response_body
{"error_msg":"failed to parse key: PEM_read_bio_PrivateKey() failed"}
=== TEST 8: bad certs
--- config
location /t {
content_by_lua_block {
local json = require("toolkit.json")
local t = require("lib.test_admin")
local ssl_cert = t.read_file("t/certs/apisix.crt")
local ssl_key = t.read_file("t/certs/apisix.key")
local data = {cert = ssl_cert, key = ssl_key, sni = "t.com",
certs = {
[[-----BEGIN CERTIFICATE-----
MIIEojCCAwqgAwIBAgIJAK253pMhgCkxMA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNV
BAYTAkNOMRIwEAYDVQQIDAlHdWFuZ0RvbmcxDzANBgNVBAcMBlpodUhhaTEPMA0G
U/OOcSRr39Kuis/JJ+DkgHYa/PWHZhnJQBxcqXXk1bJGw9BNbhM=
-----END CERTIFICATE-----]]
},
keys = {ssl_key}
}
local code, message, res = t.test('/apisix/admin/ssl/1',
ngx.HTTP_PUT,
json.encode(data)
)
if code >= 300 then
ngx.status = code
ngx.print(message)
return
end
ngx.say(res)
}
}
--- error_code: 400
--- response_body
{"error_msg":"failed to handle cert-key pair[1]: failed to parse cert: PEM_read_bio_X509_AUX() failed"}
=== TEST 9: bad keys
--- config
location /t {
content_by_lua_block {
local json = require("toolkit.json")
local t = require("lib.test_admin")
local ssl_cert = t.read_file("t/certs/apisix.crt")
local ssl_key = t.read_file("t/certs/apisix.key")
local data = {cert = ssl_cert, key = ssl_key, sni = "t.com",
certs = {ssl_cert},
keys = {[[-----BEGIN RSA PRIVATE KEY-----
MIIG5AIBAAKCAYEAyCM0rqJecvgnCfOw4fATotPwk5Ba0gC2YvIrO+gSbQkyxXF5
jhZB3W6BkWUWR4oNFLLSqcVbVDPitz/Mt46Mo8amuS6zTbQetGnBARzPLtmVhJfo
wzarryret/7GFW1/3cz+hTj9/d45i25zArr3Pocfpur5mfz3fJO8jg==
-----END RSA PRIVATE KEY-----]]}
}
local code, message, res = t.test('/apisix/admin/ssl/1',
ngx.HTTP_PUT,
json.encode(data)
)
if code >= 300 then
ngx.status = code
ngx.print(message)
return
end
ngx.say(res)
}
}
--- error_code: 400
--- response_body
{"error_msg":"failed to handle cert-key pair[1]: failed to parse key: PEM_read_bio_PrivateKey() failed"}
493 changes: 4 additions & 489 deletions t/router/radixtree-sni.t

Large diffs are not rendered by default.

356 changes: 356 additions & 0 deletions t/router/radixtree-sni2.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,356 @@
#
# 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 'no_plan';

log_level('debug');
no_root_location();

$ENV{TEST_NGINX_HTML_DIR} ||= html_dir();

run_tests;

__DATA__
=== TEST 1 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("t/certs/apisix.crt")
local ssl_key = t.read_file("t/certs/apisix.key")
local ssl_ecc_cert = t.read_file("t/certs/apisix_ecc.crt")
local ssl_ecc_key = t.read_file("t/certs/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 2: 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 3: 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
=== TEST 4: set ssl(sni: *.test2.com) once again
--- config
location /t {
content_by_lua_block {
local core = require("apisix.core")
local t = require("lib.test_admin")
local ssl_cert = t.read_file("t/certs/test2.crt")
local ssl_key = t.read_file("t/certs/test2.key")
local data = {cert = ssl_cert, key = ssl_key, sni = "*.test2.com"}
local code, body = t.test('/apisix/admin/ssl/1',
ngx.HTTP_PUT,
core.json.encode(data),
[[{
"node": {
"value": {
"sni": "*.test2.com"
},
"key": "/apisix/ssl/1"
},
"action": "set"
}]]
)
ngx.status = code
ngx.say(body)
}
}
--- request
GET /t
--- response_body
passed
--- no_error_log
[error]
=== TEST 5: caching of parsed certs and pkeys
--- config
listen unix:$TEST_NGINX_HTML_DIR/nginx.sock ssl;
location /t {
content_by_lua_block {
-- etcd sync
ngx.sleep(0.2)
local work = function()
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, "www.test2.com", false)
if not sess then
ngx.say("failed to do SSL handshake: ", err)
return
end
ngx.say("ssl handshake: ", type(sess))
local ok, err = sock:close()
ngx.say("close: ", ok, " ", err)
end -- do
work()
work()
-- collectgarbage()
}
}
--- request
GET /t
--- response_body eval
qr{connected: 1
ssl handshake: userdata
close: 1 nil
connected: 1
ssl handshake: userdata
close: 1 nil}
--- grep_error_log eval
qr/parsing (cert|(priv key)) for sni: www.test2.com/
--- grep_error_log_out
parsing cert for sni: www.test2.com
parsing priv key for sni: www.test2.com
=== TEST 6: set ssl(encrypt ssl keys with another iv)
--- config
location /t {
content_by_lua_block {
-- etcd sync
ngx.sleep(0.2)
local core = require("apisix.core")
local t = require("lib.test_admin")
local ssl_cert = t.read_file("t/certs/test2.crt")
local raw_ssl_key = t.read_file("t/certs/test2.key")
local ssl_key = t.aes_encrypt(raw_ssl_key)
local data = {
certs = { ssl_cert },
keys = { ssl_key },
snis = {"test2.com", "*.test2.com"},
cert = ssl_cert,
key = raw_ssl_key,
}
local code, body = t.test('/apisix/admin/ssl/1',
ngx.HTTP_PUT,
core.json.encode(data)
)
ngx.status = code
ngx.print(body)
}
}
--- request
GET /t
--- error_code: 400
--- response_body
{"error_msg":"failed to handle cert-key pair[1]: failed to decrypt previous encrypted key"}
=== TEST 7: set miss_head ssl certificate
--- config
location /t {
content_by_lua_block {
local core = require("apisix.core")
local t = require("lib.test_admin")
local ssl_cert = t.read_file("t/certs/incorrect.crt")
local ssl_key = t.read_file("t/certs/incorrect.key")
local data = {cert = ssl_cert, key = ssl_key, sni = "www.test.com"}
local code, body = t.test('/apisix/admin/ssl/1',
ngx.HTTP_PUT,
core.json.encode(data)
)
ngx.status = code
ngx.print(body)
}
}
--- request
GET /t
--- response_body
{"error_msg":"failed to parse cert: PEM_read_bio_X509_AUX() failed"}
--- error_code: 400
--- no_error_log
[alert]
=== TEST 8: client request without sni
--- config
listen unix:$TEST_NGINX_HTML_DIR/nginx.sock ssl;
location /t {
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
local sess, err = sock:sslhandshake(nil, nil, true)
if not sess then
ngx.say("failed to do SSL handshake: ", err)
return
end
end -- do
-- collectgarbage()
}
}
--- request
GET /t
--- response_body
failed to do SSL handshake: handshake failed
--- error_log
failed to fetch ssl config: failed to find SNI: please check if the client requests via IP or uses an outdated protocol
--- no_error_log
[alert]