Skip to content

Commit

Permalink
Merge pull request #966 from 3scale/tls-validation-poc
Browse files Browse the repository at this point in the history
TLS Client Certificate validation policy
  • Loading branch information
mikz authored Jan 25, 2019
2 parents e9c8f0a + 31e6c22 commit f2690af
Show file tree
Hide file tree
Showing 27 changed files with 1,285 additions and 37 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- "Matches" operation that can be used when defining conditionals [PR #975](https://github.com/3scale/apicast/pull/975)
- New routing policy that selects an upstream based on the request path, a header, a query argument, or a jwt claim [PR #976](https://github.com/3scale/apicast/pull/976), [PR #983](https://github.com/3scale/apicast/pull/983), [PR #984](https://github.com/3scale/apicast/pull/984), [THREESCALE-1709](https://issues.jboss.org/browse/THREESCALE-1709)
- Added "last" attribute in the mapping rules. When set to true indicates that, if the rule matches, APIcast should not try to match the rules placed after this one [PR #982](https://github.com/3scale/apicast/pull/982), [THREESCALE-1344](https://issues.jboss.org/browse/THREESCALE-1344)
- Added TLS Validation policy to verify TLS Client Certificate against a whitelist. [PR #966](https://github.com/3scale/apicast/pull/966), [THREESCALE-1671](https://issues.jboss.org/browse/THREESCALE-1671)

### Changed

Expand Down
1 change: 1 addition & 0 deletions gateway/conf/nginx.conf.liquid
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ http {
{{ "conf/server.key" | filesystem | first }}
{%- endif %};

ssl_verify_client optional_no_ca;
ssl_certificate_by_lua_block { require('apicast.executor'):ssl_certificate() }
{%- endif %}

Expand Down
7 changes: 7 additions & 0 deletions gateway/src/apicast/policy/tls_validation/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# TLS Validation policy

This policy can validate TLS Client Certificate against a whitelist.

Whitelist expects PEM formatted CA or Client certificates.
It is not necessary to have the full certificate chain, just partial matches are allowed.
For example you can add to the whitelist just leaf client certificates without the whole bundle with a CA certificate.
39 changes: 39 additions & 0 deletions gateway/src/apicast/policy/tls_validation/apicast-policy.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"$schema": "http://apicast.io/policy-v1/schema#manifest#",
"name": "TLS validation",
"summary": "Validate client TLS certificates",
"description": [
"Validate client certificates against individual certificates and CA certificates."
],
"version": "builtin",
"configuration": {
"type": "object",
"definitions": {
"certificate": {
"$id": "#/definitions/certificate",
"type": "object",
"properties": {
"pem_certificate": {
"type": "string",
"title": "PEM formatted certificate",
"description": "Certificate including the -----BEGIN CERTIFICATE----- and -----END CERTIFICATE-----"
}
}
},
"store": {
"$id": "#/definitions/store",
"type": "array",
"items": {
"$ref": "#/definitions/certificate"
}
}
},
"properties": {
"whitelist": {
"$ref": "#/definitions/store",
"title": "Certificate Whitelist",
"description": "Individual certificates and CA certificates to be whitelisted."
}
}
}
}
1 change: 1 addition & 0 deletions gateway/src/apicast/policy/tls_validation/init.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
return require('tls_validation')
63 changes: 63 additions & 0 deletions gateway/src/apicast/policy/tls_validation/tls_validation.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
-- This is a tls_validation description.

local policy = require('apicast.policy')
local _M = policy.new('tls_validation')
local X509_STORE = require('resty.openssl.x509.store')
local X509 = require('resty.openssl.x509')

local ipairs = ipairs
local tostring = tostring

local debug = ngx.config.debug

local function init_trusted_store(store, certificates)
for _,certificate in ipairs(certificates) do
local cert, err = X509.parse_pem_cert(certificate.pem_certificate) -- TODO: handle errors

if cert then
store:add_cert(cert)

if debug then
ngx.log(ngx.DEBUG, 'adding certificate to the tls validation ', tostring(cert:subject_name()), ' SHA1: ', cert:hexdigest('SHA1'))
end
else
ngx.log(ngx.WARN, 'error whitelisting certificate, err: ', err)

if debug then
ngx.log(ngx.DEBUG, 'certificate: ', certificate.pem_certificate)
end
end
end

return store
end

local new = _M.new
--- Initialize a tls_validation
-- @tparam[opt] table config Policy configuration.
function _M.new(config)
local self = new(config)
local store = X509_STORE.new()

self.x509_store = init_trusted_store(store, config and config.whitelist or {})
self.error_status = config and config.error_status or 400

return self
end

function _M:access()
local cert = X509.parse_pem_cert(ngx.var.ssl_client_raw_cert)
local store = self.x509_store

local ok, err = store:validate_cert(cert)

if not ok then
ngx.status = self.error_status
ngx.say(err)
return ngx.exit(ngx.status)
end

return ok, err
end

return _M
38 changes: 2 additions & 36 deletions gateway/src/resty/oidc/jwk.lua
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
local ipairs = ipairs
local error = error

local b64 = require('ngx.base64')
local ffi = require('ffi')
local tab_new = require('resty.core.base').new_tab
local base = require('resty.openssl.base')

ffi.cdef [[
typedef struct bio_st BIO;
Expand All @@ -29,50 +29,16 @@ void RSA_free(RSA *rsa);

int PEM_write_RSA_PUBKEY(FILE *fp, RSA *x);
int PEM_write_bio_RSA_PUBKEY(BIO *bp, RSA *x);

unsigned long ERR_get_error(void);
const char *ERR_reason_error_string(unsigned long e);
]]

local C = ffi.C
local ffi_gc = ffi.gc
local ffi_assert = base.ffi_assert

local _M = { }

_M.jwk_to_pem = { }

local function openssl_error()
local code, reason

while true do
--[[
https://www.openssl.org/docs/man1.1.0/crypto/ERR_get_error.html
ERR_get_error() returns the earliest error code
from the thread's error queue and removes the entry.
This function can be called repeatedly
until there are no more error codes to return.
]]--
code = C.ERR_get_error()

if code == 0 then
break
else
reason = C.ERR_reason_error_string(code)
end
end

return ffi.string(reason)
end

local function ffi_assert(ret, expected)
if not ret or ret == -1 or (expected and ret ~= expected) then
error(openssl_error(), 2)
end

return ret
end

local function b64toBN(str)
local val, err = b64.decode_base64url(str)
if not val then return nil, err end
Expand Down
87 changes: 87 additions & 0 deletions gateway/src/resty/openssl/base.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
local ffi = require('ffi')

ffi.cdef([[
typedef long time_t;
// https://github.com/openssl/openssl/blob/4ace4ccda2934d2628c3d63d41e79abe041621a7/include/openssl/ossl_typ.h
typedef struct x509_store_st X509_STORE;
typedef struct x509_st X509;
typedef struct X509_crl_st X509_CRL;
typedef struct X509_name_st X509_NAME;
typedef struct bio_st BIO;
typedef struct bio_method_st BIO_METHOD;
typedef struct X509_VERIFY_PARAM_st X509_VERIFY_PARAM;
typedef struct stack_st OPENSSL_STACK;
typedef struct evp_md_st {
int type;
int pkey_type;
int md_size;
} EVP_MD;
unsigned long ERR_get_error(void);
const char *ERR_reason_error_string(unsigned long e);
void ERR_clear_error(void);
]])

local C = ffi.C
local _M = { }

local error = error

local function openssl_error()
local code, reason

while true do
--[[
https://www.openssl.org/docs/man1.1.0/crypto/ERR_get_error.html
ERR_get_error() returns the earliest error code
from the thread's error queue and removes the entry.
This function can be called repeatedly
until there are no more error codes to return.
]]--
code = C.ERR_get_error()

if code == 0 then
break
else
reason = C.ERR_reason_error_string(code)
end
end

C.ERR_clear_error()

if reason then
return ffi.string(reason)
end
end

local function ffi_value(ret, expected)
if ret == nil or ret == -1 or (expected and ret ~= expected) then
return nil, openssl_error() or 'expected value, got nil'
end

return ret
end

local function ffi_assert(ret, expected)
local value, err = ffi_value(ret, expected)

if not value then
error(err, 2)
end

return value
end

local function tocdata(obj)
return obj and obj.cdata or obj
end

_M.ffi_assert = ffi_assert
_M.ffi_value = ffi_value
_M.openssl_error = openssl_error
_M.tocdata = tocdata

return _M
59 changes: 59 additions & 0 deletions gateway/src/resty/openssl/bio.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
local base = require('resty.openssl.base')
local ffi = require('ffi')

ffi.cdef([[
// https://www.openssl.org/docs/manmaster/man3/BIO_write.html
BIO_METHOD *BIO_s_mem(void);
BIO * BIO_new(BIO_METHOD *type);
void BIO_vfree(BIO *a);
int BIO_read(BIO *b, void *data, int len);
int BIO_write(BIO *b, const void *data, int dlen);
size_t BIO_ctrl_pending(BIO *b);
]])
local C = ffi.C
local ffi_assert = base.ffi_assert
local str_len = string.len
local assert = assert

local _M = {

}

local mt = {
__index = _M,
__new = function(ct, bio_method)
local bio = ffi_assert(C.BIO_new(bio_method))

return ffi.new(ct, bio)
end,
__gc = function(self)
C.BIO_vfree(self.cdata)
end,
}

-- no changes to the metamethods possible from this point
local BIO = ffi.metatype('struct { BIO *cdata; }', mt)

local bio_mem = C.BIO_s_mem()

function _M:read()
local bio = self.cdata
-- BIO_ctrl_pending() return the amount of pending data.
local len = C.BIO_ctrl_pending(bio)
local buf = ffi.new("char[?]", len)
ffi_assert(C.BIO_read(bio, buf, len) >= 0)
return ffi.string(buf, len)
end

function _M:write(str)
local len = str_len(assert(str, 'expected string'))

return ffi_assert(C.BIO_write(self.cdata, str, len))
end

function _M.new()
return BIO(bio_mem)
end

return _M
Loading

0 comments on commit f2690af

Please sign in to comment.