diff --git a/gateway/src/apicast/policy/tls_validation/apicast-policy.json b/gateway/src/apicast/policy/tls_validation/apicast-policy.json index 348f53e9d..117e87bbc 100644 --- a/gateway/src/apicast/policy/tls_validation/apicast-policy.json +++ b/gateway/src/apicast/policy/tls_validation/apicast-policy.json @@ -48,6 +48,12 @@ "title": "Certificate Revocation Check type", "type": "string", "oneOf": [ + { + "enum": [ + "ocsp" + ], + "title": "Enables OCSP validation of the client certificate." + }, { "enum": [ "crl" @@ -89,6 +95,45 @@ "$ref": "#/definitions/store" } } + }, + { + "properties": { + "revocation_check_type": { + "enum": [ + "ocsp" + ] + }, + "revocation_check_mode": { + "title": "Certificate Mode", + "description": "Certificate revocation check mode", + "type": "string", + "oneOf": [ + { + "enum": [ + "ignore_error" + ], + "title": "Ignore Network Error: respects the revocation status when either OCSP or CRL URL is set, and doesn’t fail on network issues" + }, + { + "enum": [ + "strict" + ], + "title": "Strict: The certificate is valid only when it’s able to verify the revocation status." + } + ], + "default": "strict" + }, + "ocsp_responder_url": { + "title": "OCSP Responder URL ", + "description": "Overrides the URL of the OCSP responder specified in the “Authority Information Access” certificate extension for validation of client certificates. ", + "type": "string" + }, + "cache_timeout": { + "title": " Cache timeout", + "description": "The length of time in milliseconds between refreshes of the revocation check status cache.", + "type": "integer" + } + } } ] } diff --git a/gateway/src/apicast/policy/tls_validation/ocsp.lua b/gateway/src/apicast/policy/tls_validation/ocsp.lua new file mode 100644 index 000000000..b0ffe897e --- /dev/null +++ b/gateway/src/apicast/policy/tls_validation/ocsp.lua @@ -0,0 +1,92 @@ +local http_ng = require 'resty.http_ng' +local user_agent = require 'apicast.user_agent' +local resty_env = require('resty.env') +local tls = require "resty.tls" +local ngx_ssl = require "ngx.ssl" +local ocsp = require("ngx.ocsp") + +local _M = {} + +local function do_ocsp_request(ocsp_url, ocsp_request) + -- TODO: set default timeout + local http_client = http_ng.new{ + options = { + headers = { + ['User-Agent'] = user_agent() + }, + ssl = { verify = resty_env.enabled('OPENSSL_VERIFY') } + } + } + local res, err = http_client.post{ + ocsp_url, + ocsp_request, + headers= { + ["Content-Type"] = "application/ocsp-request" + }} + if err then + return nil, err + end + + if not res then + return nil, "failed to send request to OCSP responder: " .. tostring(err) + end + + if res.status ~= 200 then + return nil, "unexpected OCSP responder status code: " .. res.status + end + + return res.body +end + +function _M:check_revocation_status() + local cert_chain, err = tls.get_full_client_certificate_chain() + if not cert_chain then + return nil, err or "no client certificate" + end + + local der_cert + der_cert, err = ngx_ssl.cert_pem_to_der(cert_chain) + if not der_cert then + return nil, "failed to convert certificate chain from PEM to DER " .. err + end + + -- TODO: check response cache + local ocsp_url + ocsp_url, err = ocsp.get_ocsp_responder_from_der_chain(der_cert) + if not ocsp_url then + return nil, error or ("could not extract OCSP responder URL, the client " .. + "certificate may be missing the required extensions") + end + + local ocsp_req + ocsp_req, err = ocsp.create_ocsp_request(der_cert) + if not ocsp_req then + return nil, "failed to create OCSP request: " .. err + end + + local ocsp_resp + ocsp_resp, err = do_ocsp_request(ocsp_url, ocsp_req) + if not ocsp_req then + return nil, "failed to get OCSP response: " .. err + end + if not ocsp_resp or #ocsp_resp == 0 then + return nil, "unexpected response from OCSP responder: empty body" + end + + local ok + ok, err = ocsp.validate_ocsp_response(ocsp_resp, der_cert) + if not ok then + return false, "failed to validate OCSP response: " .. err + end + + -- TODO: cache the response + -- Use ttl, normally this should be (nextUpdate - thisUpdate), but current version + -- of openresty API does not expose those attributes. Support for this was added + -- in openrest-core v0.1.31, we either need to backport or upgrade the openresty + -- version + -- + -- TODO: use cert digest or uid instead + return true +end + +return _M diff --git a/gateway/src/apicast/policy/tls_validation/tls_validation.lua b/gateway/src/apicast/policy/tls_validation/tls_validation.lua index fb6b63408..174aa1d33 100644 --- a/gateway/src/apicast/policy/tls_validation/tls_validation.lua +++ b/gateway/src/apicast/policy/tls_validation/tls_validation.lua @@ -6,6 +6,7 @@ local X509_STORE = require('resty.openssl.x509.store') local X509 = require('resty.openssl.x509') local X509_CRL = require('resty.openssl.x509.crl') local ngx_ssl = require "ngx.ssl" +local ocsp = require ("ocsp") local ipairs = ipairs local tostring = tostring @@ -85,6 +86,7 @@ function _M:ssl_certificate() -- provide ca_certs: See https://github.com/openresty/lua-resty-core/blob/master/lib/ngx/ssl.md#verify_client -- handle verify_depth -- + -- TODO: OCSP stapling return ngx_ssl.verify_client() end @@ -122,6 +124,16 @@ function _M:access() return ngx.exit(ngx.status) end + if self.revocation_type == "ocsp" then + ok, err = ocsp:check_for_revocation_status() + if not ok then + ngx.status = self.error_status + ngx.log(ngx.WARN, "TLS certificate validation failed, err: ", err) + ngx.say("TLS certificate validation failed") + return ngx.exit(ngx.status) + end + end + return true, nil end diff --git a/gateway/src/resty/tls.lua b/gateway/src/resty/tls.lua index 0b5a65e7f..bb2a3db6f 100644 --- a/gateway/src/resty/tls.lua +++ b/gateway/src/resty/tls.lua @@ -4,18 +4,28 @@ local type = type local tostring = tostring local get_request = base.get_request +local get_size_ptr = base.get_size_ptr local ffi = require "ffi" +local ffi_new = ffi.new +local ffi_str = ffi.string local C = ffi.C local _M = {} local NGX_OK = ngx.OK +local NGX_ERROR = ngx.ERROR +local NGX_DECLINED = ngx.DECLINED +local ngx_http_apicast_ffi_get_full_client_certificate_chain; local ngx_http_apicast_ffi_set_proxy_cert_key; local ngx_http_apicast_ffi_set_proxy_ca_cert; local ngx_http_apicast_ffi_set_ssl_verify +local value_ptr = ffi_new("unsigned char *[1]") + ffi.cdef([[ + int ngx_http_apicast_ffi_get_full_client_certificate_chain( + ngx_http_request_t *r, char **value, size_t *value_len); int ngx_http_apicast_ffi_set_proxy_cert_key( ngx_http_request_t *r, void *cdata_chain, void *cdata_key); int ngx_http_apicast_ffi_set_proxy_ca_cert( @@ -24,6 +34,7 @@ ffi.cdef([[ ngx_http_request_t *r, int verify, int verify_deph); ]]) +ngx_http_apicast_ffi_get_full_client_certificate_chain = C.ngx_http_apicast_ffi_get_full_client_certificate_chain ngx_http_apicast_ffi_set_proxy_cert_key = C.ngx_http_apicast_ffi_set_proxy_cert_key ngx_http_apicast_ffi_set_proxy_ca_cert = C.ngx_http_apicast_ffi_set_proxy_ca_cert ngx_http_apicast_ffi_set_ssl_verify = C.ngx_http_apicast_ffi_set_ssl_verify @@ -88,4 +99,29 @@ function _M.set_upstream_ssl_verify(verify, verify_deph) end end +-- Retrieve the full client certificate chain +function _M.get_full_client_certificate_chain() + local r = get_request() + if not r then + error("no request found") + end + + local size_ptr = get_size_ptr() + + local rc = ngx_http_apicast_ffi_get_full_client_certificate_chain(r, value_ptr, size_ptr) + + if rc == NGX_OK then + return ffi_str(value_ptr[0], size_ptr[0]) + end + + if rc == NGX_ERROR then + return nil, "error while obtaining client certificate chain" + end + + + if rc == NGX_DECLINED then + return nil + end +end + return _M