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: add ocsp-stapling plugin #10817

Merged
merged 31 commits into from
Jan 24, 2024
Merged
Show file tree
Hide file tree
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
2 changes: 2 additions & 0 deletions .licenserc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -54,5 +54,7 @@ header:
- 'docs/**/*.md'
- '.ignore_words'
- '.luacheckrc'
# Exclude file contains certificate revocation information
- 't/certs/ocsp/index.txt'

comment: on-failure
4 changes: 4 additions & 0 deletions apisix/cli/ngx_tpl.lua
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,10 @@ http {
lua_shared_dict access-tokens {* http.lua_shared_dict["access-tokens"] *}; # cache for service account access tokens
{% end %}

{% if enabled_plugins["ocsp-stapling"] then %}
lua_shared_dict ocsp-stapling {* http.lua_shared_dict["ocsp-stapling"] *}; # cache for ocsp-stapling
{% end %}

{% if enabled_plugins["ext-plugin-pre-req"] or enabled_plugins["ext-plugin-post-req"] then %}
lua_shared_dict ext-plugin {* http.lua_shared_dict["ext-plugin"] *}; # cache for ext-plugin
{% end %}
Expand Down
2 changes: 2 additions & 0 deletions apisix/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ function _M.http_ssl_client_hello_phase()
core.log.error("failed to find SNI: " .. (err or advise))
ngx_exit(-1)
end
local tls_ext_status_req = apisix_ssl.get_status_request_ext()

local ngx_ctx = ngx.ctx
local api_ctx = core.tablepool.fetch("api_ctx", 0, 32)
Expand All @@ -201,6 +202,7 @@ function _M.http_ssl_client_hello_phase()
ngx_ctx.matched_ssl = api_ctx.matched_ssl
core.tablepool.release("api_ctx", api_ctx)
ngx_ctx.api_ctx = nil
ngx_ctx.tls_ext_status_req = tls_ext_status_req

if not ok then
if err then
Expand Down
220 changes: 220 additions & 0 deletions apisix/plugins/ocsp-stapling.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
--
-- 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 require = require
local http = require("resty.http")
local ngx = ngx
local ngx_ocsp = require("ngx.ocsp")
local ngx_ssl = require("ngx.ssl")
local radixtree_sni = require("apisix.ssl.router.radixtree_sni")
local core = require("apisix.core")

local plugin_name = "ocsp-stapling"
local ocsp_resp_cache = ngx.shared[plugin_name]

local plugin_schema = {
type = "object",
properties = {},
}

local _M = {
name = plugin_name,
schema = plugin_schema,
version = 0.1,
priority = -44,
}


function _M.check_schema(conf)
return core.schema.check(plugin_schema, conf)
end


local function fetch_ocsp_resp(der_cert_chain)
core.log.info("fetch ocsp response from remote")
local ocsp_url, err = ngx_ocsp.get_ocsp_responder_from_der_chain(der_cert_chain)

if not ocsp_url then
-- if cert not support ocsp, the report error is nil
if not err then
err = "cert not contains authority_information_access extension"
end
return nil, "failed to get ocsp url: " .. err
end

local ocsp_req, err = ngx_ocsp.create_ocsp_request(der_cert_chain)
if not ocsp_req then
return nil, "failed to create ocsp request: " .. err
end

local httpc = http.new()
local res, err = httpc:request_uri(ocsp_url, {
method = "POST",
headers = {
["Content-Type"] = "application/ocsp-request",
},
body = ocsp_req
})

if not res then
return nil, "ocsp responder query failed: " .. err
end

local http_status = res.status
if http_status ~= 200 then
return nil, "ocsp responder returns bad http status code: "
.. http_status
end

if res.body and #res.body > 0 then
return res.body, nil
end

return nil, "ocsp responder returns empty body"
end


local function set_ocsp_resp(full_chain_pem_cert, skip_verify, cache_ttl)
local der_cert_chain, err = ngx_ssl.cert_pem_to_der(full_chain_pem_cert)
if not der_cert_chain then
return false, "failed to convert certificate chain from PEM to DER: ", err
end

local ocsp_resp = ocsp_resp_cache:get(der_cert_chain)
if ocsp_resp == nil then
core.log.info("not ocsp resp cache found, fetch from ocsp responder")
ocsp_resp, err = fetch_ocsp_resp(der_cert_chain)
if ocsp_resp == nil then
return false, err
end
core.log.info("fetch ocsp resp ok, cache it")
ocsp_resp_cache:set(der_cert_chain, ocsp_resp, cache_ttl)
end

if not skip_verify then
local ok, err = ngx_ocsp.validate_ocsp_response(ocsp_resp, der_cert_chain)
if not ok then
return false, "failed to validate ocsp response: " .. err
end
end

-- set the OCSP stapling
local ok, err = ngx_ocsp.set_ocsp_status_resp(ocsp_resp)
if not ok then
return false, "failed to set ocsp status response: " .. err
end

return true
end


local original_set_cert_and_key
local function set_cert_and_key(sni, value)
if value.gm then
-- should not run with gm plugin
core.log.warn("gm plugin enabled, no need to run ocsp-stapling plugin")
return original_set_cert_and_key(sni, value)
end

if not value.ocsp_stapling then
core.log.info("no 'ocsp_stapling' field found, no need to run ocsp-stapling plugin")
return original_set_cert_and_key(sni, value)
end

if not value.ocsp_stapling.enabled then
return original_set_cert_and_key(sni, value)
end

if not ngx.ctx.tls_ext_status_req then
core.log.info("no status request required, no need to send ocsp response")
return original_set_cert_and_key(sni, value)
end

local ok, err = radixtree_sni.set_pem_ssl_key(sni, value.cert, value.key)
if not ok then
return false, err
end
local fin_pem_cert = value.cert

-- multiple certificates support.
if value.certs then
for i = 1, #value.certs do
local cert = value.certs[i]
local key = value.keys[i]
ok, err = radixtree_sni.set_pem_ssl_key(sni, cert, key)
if not ok then
return false, err
end
fin_pem_cert = cert
end
end

local ok, err = set_ocsp_resp(fin_pem_cert,
value.ocsp_stapling.skip_verify,
value.ocsp_stapling.cache_ttl)
if not ok then
core.log.error("no ocsp response send: ", err)
end

return true
end


function _M.init()
if core.schema.ssl.properties.gm ~= nil then
core.log.error("ocsp-stapling plugin should not run with gm plugin")
end

original_set_cert_and_key = radixtree_sni.set_cert_and_key
radixtree_sni.set_cert_and_key = set_cert_and_key

if core.schema.ssl.properties.ocsp_stapling ~= nil then
core.log.error("Field 'ocsp_stapling' is occupied")
end

core.schema.ssl.properties.ocsp_stapling = {
type = "object",
properties = {
enabled = {
type = "boolean",
default = false,
},
skip_verify = {
type = "boolean",
default = false,
},
cache_ttl = {
type = "integer",
minimum = 60,
default = 3600,
},
}
}

end


function _M.destroy()
radixtree_sni.set_cert_and_key = original_set_cert_and_key
core.schema.ssl.properties.ocsp_stapling = nil
ocsp_resp_cache:flush_all()
end


return _M
31 changes: 31 additions & 0 deletions apisix/ssl.lua
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ local ngx_encode_base64 = ngx.encode_base64
local ngx_decode_base64 = ngx.decode_base64
local aes = require("resty.aes")
local str_lower = string.lower
local str_byte = string.byte
local str_len = string.len
local assert = assert
local type = type
local ipairs = ipairs
Expand Down Expand Up @@ -313,4 +315,33 @@ function _M.check_ssl_conf(in_dp, conf)
end


function _M.get_status_request_ext()
core.log.debug("parsing status request extension ... ")
local ext = ngx_ssl_client.get_client_hello_ext(5)
if not ext then
core.log.debug("no contains status request extension")
return false
end
local total_len = str_len(ext)
-- 1-byte for CertificateStatusType
-- 2-byte for zero-length "responder_id_list"
-- 2-byte for zero-length "request_extensions"
if total_len < 5 then
core.log.error("bad ssl client hello extension: ",
"extension data error")
return false
end

-- CertificateStatusType
local status_type = str_byte(ext, 1)
if status_type == 1 then
core.log.debug("parsing status request extension ok: ",
"status_type is ocsp(1)")
return true
end

return false
end


return _M
1 change: 1 addition & 0 deletions apisix/ssl/router/radixtree_sni.lua
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ local function set_pem_ssl_key(sni, cert, pkey)

return true
end
_M.set_pem_ssl_key = set_pem_ssl_key


-- export the set cert/key process so we can hook it in the other plugins
Expand Down
2 changes: 2 additions & 0 deletions conf/config-default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ nginx_config: # Config for render the template to generate n
ext-plugin: 1m
tars: 1m
cas-auth: 10m
ocsp-stapling: 10m

# discovery: # Service Discovery
# dns:
Expand Down Expand Up @@ -524,6 +525,7 @@ plugins: # plugin list (sorted by priority)
# <- recommend to use priority (0, 100) for your custom plugins
- example-plugin # priority: 0
#- gm # priority: -43
#- ocsp-stapling # priority: -44
- aws-lambda # priority: -1899
- azure-functions # priority: -1900
- openwhisk # priority: -1901
Expand Down
1 change: 1 addition & 0 deletions t/APISIX.pm
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,7 @@ _EOC_
lua_shared_dict kubernetes-first 1m;
lua_shared_dict kubernetes-second 1m;
lua_shared_dict tars 1m;
lua_shared_dict ocsp-stapling 10m;
lua_shared_dict xds-config 1m;
lua_shared_dict xds-config-version 1m;
lua_shared_dict cas_sessions 10m;
Expand Down
45 changes: 45 additions & 0 deletions t/certs/ocsp/ecc_good.crt
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
-----BEGIN CERTIFICATE-----
MIIC+jCCAWKgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJDTjES
MBAGA1UECAwJR3VhbmdEb25nMQ8wDQYDVQQHDAZaaHVIYWkxDzANBgNVBAoMBmly
ZXN0eTERMA8GA1UEAwwIdGVzdC5jb20wHhcNMjQwMTEyMTQ1OTUwWhcNMzQwMTA5
MTQ1OTUwWjA9MQswCQYDVQQGEwJDTjEWMBQGA1UECwwNQXBhY2hlIEFQSVNJWDEW
MBQGA1UEAwwNb2NzcC50ZXN0LmNvbTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IA
BIuZCmq/vmX5LqFrpa9ot5SboEhNyS/r5UGT7akjIOAXBVwZkn1vm/EsQp9VMF8y
rWZkGKFmElo0ZAXAyhjn9D6jNzA1MDMGCCsGAQUFBwEBBCcwJTAjBggrBgEFBQcw
AYYXaHR0cDovLzEyNy4wLjAuMToxMTQ1MS8wDQYJKoZIhvcNAQELBQADggGBAIEm
LKKS+eGBazPpSRvq2agnqmjM+PHVWRB/O/+LNOO69Lji3wRtq6T2zNHPZQXw1OMA
3C9HcIwaawTyb+hm+vX8yBr5mgS1UOtmDYzbnlpERjJBjxmPXTZLDbzogHshbabp
227p/IAjWm/2F2VPXjiX+aV1pYrhCcO7zUtBEu9KaoG3Amxg8T2WVamTV+J6r0SL
fkvYItZwbawSfwQlZ+22H4Mttu/bd2USTusT4zLAflv9UFh20bA1PizvcKK1brWS
IH2rxxSLCvu2wmrGsrLVn+9yD6xNsn4m6DyCWx9S/Tas7KLub8BjnCzP8YEvrVpV
fotefEMY5h0waj9Zc32l+6gk8Ntyp2ozWi+iu4eo0Y5SUqHlPjuGUXOivp5o/6b0
gF5M9jtkXvbH2ffrOiz9YUo4fVwk6ws5OQTr9WsildEHZH4ADOW6HqPYkOnuxhdM
p6JP0LmnO/S60/k/ZH8nMTcSUfE+qcDg3LlH5ay2fv6IKz5BaVkyHPNreRi9qg==
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIEojCCAwqgAwIBAgIJAK253pMhgCkxMA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNV
BAYTAkNOMRIwEAYDVQQIDAlHdWFuZ0RvbmcxDzANBgNVBAcMBlpodUhhaTEPMA0G
A1UECgwGaXJlc3R5MREwDwYDVQQDDAh0ZXN0LmNvbTAgFw0xOTA2MjQyMjE4MDVa
GA8yMTE5MDUzMTIyMTgwNVowVjELMAkGA1UEBhMCQ04xEjAQBgNVBAgMCUd1YW5n
RG9uZzEPMA0GA1UEBwwGWmh1SGFpMQ8wDQYDVQQKDAZpcmVzdHkxETAPBgNVBAMM
CHRlc3QuY29tMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAyCM0rqJe
cvgnCfOw4fATotPwk5Ba0gC2YvIrO+gSbQkyxXF5jhZB3W6BkWUWR4oNFLLSqcVb
VDPitz/Mt46Mo8amuS6zTbQetGnBARzPLtmVhJfoeLj0efMiOepOSZflj9Ob4yKR
2bGdEFOdHPjm+4ggXU9jMKeLqdVvxll/JiVFBW5smPtW1Oc/BV5terhscJdOgmRr
abf9xiIis9/qVYfyGn52u9452V0owUuwP7nZ01jt6iMWEGeQU6mwPENgvj1olji2
WjdG2UwpUVp3jp3l7j1ekQ6mI0F7yI+LeHzfUwiyVt1TmtMWn1ztk6FfLRqwJWR/
Evm95vnfS3Le4S2ky3XAgn2UnCMyej3wDN6qHR1onpRVeXhrBajbCRDRBMwaNw/1
/3Uvza8QKK10PzQR6OcQ0xo9psMkd9j9ts/dTuo2fzaqpIfyUbPST4GdqNG9NyIh
/B9g26/0EWcjyO7mYVkaycrtLMaXm1u9jyRmcQQI1cGrGwyXbrieNp63AgMBAAGj
cTBvMB0GA1UdDgQWBBSZtSvV8mBwl0bpkvFtgyiOUUcbszAfBgNVHSMEGDAWgBSZ
tSvV8mBwl0bpkvFtgyiOUUcbszAMBgNVHRMEBTADAQH/MB8GA1UdEQQYMBaCCHRl
c3QuY29tggoqLnRlc3QuY29tMA0GCSqGSIb3DQEBCwUAA4IBgQAHGEul/x7ViVgC
tC8CbXEslYEkj1XVr2Y4hXZXAXKd3W7V3TC8rqWWBbr6L/tsSVFt126V5WyRmOaY
1A5pju8VhnkhYxYfZALQxJN2tZPFVeME9iGJ9BE1wPtpMgITX8Rt9kbNlENfAgOl
PYzrUZN1YUQjX+X8t8/1VkSmyZysr6ngJ46/M8F16gfYXc9zFj846Z9VST0zCKob
rJs3GtHOkS9zGGldqKKCj+Awl0jvTstI4qtS1ED92tcnJh5j/SSXCAB5FgnpKZWy
hme45nBQj86rJ8FhN+/aQ9H9/2Ib6Q4wbpaIvf4lQdLUEcWAeZGW6Rk0JURwEog1
7/mMgkapDglgeFx9f/XztSTrkHTaX4Obr+nYrZ2V4KOB4llZnK5GeNjDrOOJDk2y
IJFgBOZJWyS93dQfuKEj42hA79MuX64lMSCVQSjX+ipR289GQZqFrIhiJxLyA+Ve
U/OOcSRr39Kuis/JJ+DkgHYa/PWHZhnJQBxcqXXk1bJGw9BNbhM=
-----END CERTIFICATE-----
8 changes: 8 additions & 0 deletions t/certs/ocsp/ecc_good.key
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
-----BEGIN EC PARAMETERS-----
BggqhkjOPQMBBw==
-----END EC PARAMETERS-----
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIHMwGqSAcIFnsy8Sa6NxlSmGuOXV13SbZbZVIobN+3xboAoGCCqGSM49
AwEHoUQDQgAEi5kKar++ZfkuoWulr2i3lJugSE3JL+vlQZPtqSMg4BcFXBmSfW+b
8SxCn1UwXzKtZmQYoWYSWjRkBcDKGOf0Pg==
-----END EC PRIVATE KEY-----
4 changes: 4 additions & 0 deletions t/certs/ocsp/index.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
V 340109124821Z 1 unknown /C=CN/OU=Apache APISIX/CN=ocsp.test.com1
V 340109125024Z 2 unknown /C=CN/OU=Apache APISIX/CN=ocsp.test.com2
R 340109125151Z 240109125151Z 3 unknown /C=CN/OU=Apache APISIX/CN=ocsp-revoked.test.com
V 340109125746Z 5 unknown /C=CN/OU=Apache APISIX/CN=ocsp test CA signer
Loading
Loading