From 9e93adfac1a47ce1e86eccdf0c3b9b8355e0258a Mon Sep 17 00:00:00 2001 From: Michal Cichra Date: Tue, 15 Jan 2019 19:13:17 +0700 Subject: [PATCH] APIcast TLS policy Extracted from https://github.com/3scale/apicast/pull/966 --- .s2i/bin/assemble | 2 +- README.md | 15 ++- Roverfile | 9 -- Roverfile.lock | 16 --- apicast.d/ssl_verify_client.conf | 1 + cpanfile | 1 - openshift.yml | 19 ++-- policies/example/0.1/apicast-policy.json | 11 -- policies/example/0.1/example.lua | 51 --------- policies/example/0.1/init.lua | 1 - .../tls_validation/0.1/apicast-policy.json | 39 +++++++ policies/tls_validation/0.1/init.lua | 1 + .../tls_validation/0.1/tls_validation.lua | 61 +++++++++++ resty/openssl/base.lua | 85 +++++++++++++++ resty/openssl/bio.lua | 59 ++++++++++ resty/openssl/evp.lua | 61 +++++++++++ resty/openssl/x509.lua | 102 +++++++++++++++++ resty/openssl/x509/name.lua | 103 ++++++++++++++++++ resty/openssl/x509/store.lua | 82 ++++++++++++++ resty/openssl/x509/store/ctx.lua | 53 +++++++++ t/example.t | 35 ------ 21 files changed, 668 insertions(+), 139 deletions(-) delete mode 100644 Roverfile delete mode 100644 Roverfile.lock create mode 100644 apicast.d/ssl_verify_client.conf delete mode 100644 cpanfile delete mode 100644 policies/example/0.1/apicast-policy.json delete mode 100644 policies/example/0.1/example.lua delete mode 100644 policies/example/0.1/init.lua create mode 100644 policies/tls_validation/0.1/apicast-policy.json create mode 100644 policies/tls_validation/0.1/init.lua create mode 100644 policies/tls_validation/0.1/tls_validation.lua create mode 100644 resty/openssl/base.lua create mode 100644 resty/openssl/bio.lua create mode 100644 resty/openssl/evp.lua create mode 100644 resty/openssl/x509.lua create mode 100644 resty/openssl/x509/name.lua create mode 100644 resty/openssl/x509/store.lua create mode 100644 resty/openssl/x509/store/ctx.lua delete mode 100644 t/example.t diff --git a/.s2i/bin/assemble b/.s2i/bin/assemble index c908ee8..5ee3b4a 100755 --- a/.s2i/bin/assemble +++ b/.s2i/bin/assemble @@ -1,4 +1,4 @@ #!/bin/sh echo "---> Copying policy source..." -cp -Rf /tmp/src/. ./ +cp -Rfv /tmp/src/. ./ diff --git a/README.md b/README.md index 8782e5b..9e02452 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,25 @@ -[![CircleCI](https://circleci.com/gh/3scale/apicast-example-policy.svg?style=svg)](https://circleci.com/gh/3scale/apicast-example-policy) - -# APIcast Example Policy - -This policy is an example how to make custom policies for APIcast. +# APIcast TLS Validation Policy +This policy validates TLS Client Certificates. ## OpenShift To install this on OpenShift you can use provided template: ```shell -oc new-app -f openshift.yml --param AMP_RELEASE=2.2.0 +oc new-app -f openshift.yml --param AMP_RELEASE=2.4.0 ``` The template creates new ImageStream for images containing this policy. Then it creates two BuildConfigs: one for building an image to apicast-policy ImageStream and second one for creating new APIcast image copying just necessary code from that previous image. +Run them both to create a new image: + +```shell +oc start-build apicast-tls-policy --follow +oc start-build apicast-custom-policies --follow +``` # License diff --git a/Roverfile b/Roverfile deleted file mode 100644 index 8bf5347..0000000 --- a/Roverfile +++ /dev/null @@ -1,9 +0,0 @@ -luarocks { - group { 'development', 'test' } { - module { 'apicast' }, - -- to be able to run repl as `require('resty.repl').start()` - module { 'lua-resty-repl' }, - -- to automatically validate policy manifests - module { 'ljsonschema' }, - } -} diff --git a/Roverfile.lock b/Roverfile.lock deleted file mode 100644 index 306b876..0000000 --- a/Roverfile.lock +++ /dev/null @@ -1,16 +0,0 @@ -apicast scm-1|ef868e51efe32b68b9c4ba5eaa80d975e289e49e|development,test -argparse 0.5.0-1||development,test -inspect 3.1.1-0||development,test -liquid 0.1.0-1||development,test -ljsonschema 0.1.0-1||development,test -lua-resty-env 0.4.0-1||development,test -lua-resty-execvp 0.1.1-1||development,test -lua-resty-http 0.12-0||development,test -lua-resty-jwt 0.1.11-0||development,test -lua-resty-repl 0.0.6-0|3878f41b7e8f97b1c96919db19dbee9496569dda|development,test -lua-resty-url 0.2.0-1||development,test -luafilesystem 1.7.0-2||development,test -net-url 0.9-1||development,test -nginx-lua-prometheus 0.20171117-4||development,test -penlight 1.5.4-1||development,test -router 2.1-0||development,test \ No newline at end of file diff --git a/apicast.d/ssl_verify_client.conf b/apicast.d/ssl_verify_client.conf new file mode 100644 index 0000000..4ba6799 --- /dev/null +++ b/apicast.d/ssl_verify_client.conf @@ -0,0 +1 @@ +ssl_verify_client optional_no_ca; diff --git a/cpanfile b/cpanfile deleted file mode 100644 index 8500ead..0000000 --- a/cpanfile +++ /dev/null @@ -1 +0,0 @@ -requires 'Test::APIcast', '0.11'; diff --git a/openshift.yml b/openshift.yml index 7724b32..16231d2 100644 --- a/openshift.yml +++ b/openshift.yml @@ -1,7 +1,7 @@ apiVersion: v1 kind: Template metadata: - name: "apicast-example-policy" + name: "apicast-tls-policy" message: "APIcast Example Policy" objects: @@ -19,15 +19,15 @@ objects: annotations: labels: app: apicast - name: apicast-example-policy + name: apicast-tls-policy spec: output: to: kind: ImageStreamTag - name: apicast-policy:example + name: apicast-policy:tls source: git: - uri: https://github.com/3scale/apicast-example-policy.git + uri: https://github.com/mikz/apicast-tls-validation-policy.git ref: 'master' type: Git strategy: @@ -62,14 +62,17 @@ objects: images: - from: kind: ImageStreamTag - name: 'apicast-policy:example' + name: 'apicast-policy:tls' paths: # copy policy source code into the new image - destinationDir: policies - sourcePath: /opt/app-root/policies/example + sourcePath: /opt/app-root/policies/tls_validation # copy also installed dependencies to the policy folder, so they are vendored - # - destinationDir: policies/example/0.1/resty/ - # sourcePath: /opt/app-root/src/lua_modules/share/lua/5.1/resty/iputils.lua + - destinationDir: policies/tls_validation/0.1/ + sourcePath: /opt/app-root/resty/ + # copy nginx configuration customization + - destinationDir: apicast.d + sourcePath: /opt/app-root/apicast.d type: Dockerfile dockerfile: | FROM scratch diff --git a/policies/example/0.1/apicast-policy.json b/policies/example/0.1/apicast-policy.json deleted file mode 100644 index d8d136d..0000000 --- a/policies/example/0.1/apicast-policy.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "$schema": "http://apicast.io/policy-v1/schema#manifest#", - "name": "APIcast Example Policy", - "summary": "This is just an example.", - "description": "This policy is just an example how to write your custom policy.", - "version": "0.1", - "configuration": { - "type": "object", - "properties": { } - } -} diff --git a/policies/example/0.1/example.lua b/policies/example/0.1/example.lua deleted file mode 100644 index 8fd47ce..0000000 --- a/policies/example/0.1/example.lua +++ /dev/null @@ -1,51 +0,0 @@ -local setmetatable = setmetatable - -local _M = require('apicast.policy').new('Example', '0.1') -local mt = { __index = _M } - -function _M.new() - return setmetatable({}, mt) -end - -function _M:init() - -- do work when nginx master process starts -end - -function _M:init_worker() - -- do work when nginx worker process is forked from master -end - -function _M:rewrite() - -- change the request before it reaches upstream -end - -function _M:access() - -- ability to deny the request before it is sent upstream -end - -function _M:content() - -- can create content instead of connecting to upstream -end - -function _M:post_action() - -- do something after the response was sent to the client -end - -function _M:header_filter() - -- can change response headers -end - -function _M:body_filter() - -- can read and change response body - -- https://github.com/openresty/lua-nginx-module/blob/master/README.markdown#body_filter_by_lua -end - -function _M:log() - -- can do extra logging -end - -function _M:balancer() - -- use for example require('resty.balancer.round_robin').call to do load balancing -end - -return _M diff --git a/policies/example/0.1/init.lua b/policies/example/0.1/init.lua deleted file mode 100644 index 3aaeece..0000000 --- a/policies/example/0.1/init.lua +++ /dev/null @@ -1 +0,0 @@ -return require('example') diff --git a/policies/tls_validation/0.1/apicast-policy.json b/policies/tls_validation/0.1/apicast-policy.json new file mode 100644 index 0000000..a01b7e3 --- /dev/null +++ b/policies/tls_validation/0.1/apicast-policy.json @@ -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": "0.1", + "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." + } + } + } +} diff --git a/policies/tls_validation/0.1/init.lua b/policies/tls_validation/0.1/init.lua new file mode 100644 index 0000000..51db51a --- /dev/null +++ b/policies/tls_validation/0.1/init.lua @@ -0,0 +1 @@ +return require('tls_validation') diff --git a/policies/tls_validation/0.1/tls_validation.lua b/policies/tls_validation/0.1/tls_validation.lua new file mode 100644 index 0000000..814b4c9 --- /dev/null +++ b/policies/tls_validation/0.1/tls_validation.lua @@ -0,0 +1,61 @@ +-- 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.var.cached_key = nil + ngx.status = self.error_status + ngx.say(err) + return ngx.exit(ngx.status) + end +end + +return _M diff --git a/resty/openssl/base.lua b/resty/openssl/base.lua new file mode 100644 index 0000000..be9ea52 --- /dev/null +++ b/resty/openssl/base.lua @@ -0,0 +1,85 @@ +local ffi = require('ffi') + +ffi.cdef([[ + // 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 diff --git a/resty/openssl/bio.lua b/resty/openssl/bio.lua new file mode 100644 index 0000000..5fb00b1 --- /dev/null +++ b/resty/openssl/bio.lua @@ -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 diff --git a/resty/openssl/evp.lua b/resty/openssl/evp.lua new file mode 100644 index 0000000..2e6a656 --- /dev/null +++ b/resty/openssl/evp.lua @@ -0,0 +1,61 @@ +local ffi = require('ffi') +local base = require('resty.openssl.base') + +ffi.cdef([[ +const EVP_MD *EVP_sha1(void); +const EVP_MD *EVP_sha256(void); +const EVP_MD *EVP_sha512(void); +const EVP_MD *EVP_get_digestbyname(const char *name); +]]) + +local C = ffi.C +local assert = assert +local tocdata = base.tocdata + +local _M = { } + +local function find(name) + local md = C.EVP_get_digestbyname(name) + + if not md then + return nil, 'not found' + end + + return md +end + +local mt = { + __index = _M, + + __new = function(ct, md) + return ffi.new(ct, assert(md)) + end, + + __len = function(self) + return tocdata(self).md_size + end, +} + +local EVP_MD = ffi.metatype('struct { const EVP_MD *cdata; }', mt) + +function _M.new(name) + local md, err = find(name) + + if not md then return nil, err end + + return EVP_MD(md) +end + +function _M.sha1() + return _M.new('SHA1') +end + +function _M.sha256() + return _M.new('SHA256') +end + +function _M.sha512() + return _M.new('SHA512') +end + +return _M diff --git a/resty/openssl/x509.lua b/resty/openssl/x509.lua new file mode 100644 index 0000000..b1c0c15 --- /dev/null +++ b/resty/openssl/x509.lua @@ -0,0 +1,102 @@ +local base = require('resty.openssl.base') +local BIO = require('resty.openssl.bio') +local X509_NAME = require('resty.openssl.x509.name') +local EVP_MD = require('resty.openssl.evp') +local resty_str = require('resty.string') +local ffi = require('ffi') +local re_gsub = ngx.re.gsub + +ffi.cdef([[ +int OPENSSL_sk_num(const OPENSSL_STACK *); +void *OPENSSL_sk_value(const OPENSSL_STACK *, int); +void *OPENSSL_sk_shift(OPENSSL_STACK *st); + +X509 *PEM_read_bio_X509(BIO *bp, X509 **x, pem_password_cb *cb, void *u); +X509_NAME *X509_get_subject_name(const X509 *x); +X509_NAME *X509_get_issuer_name(const X509 *x); + +X509 *X509_new(void); +void X509_free(X509 *a); + +int X509_digest(const X509 *data, const EVP_MD *type, unsigned char *md, unsigned int *len); +]]) + +local C = ffi.C +local openssl_error = base.openssl_error +local ffi_assert = base.ffi_assert +local tocdata = base.tocdata +local assert = assert +local _M = {} +local mt = { + __index = _M, + __new = function(ct, x509) + if x509 == nil then + return nil, openssl_error() + else + return ffi.new(ct, x509) + end + end, + __gc = function(self) + C.X509_free(self.cdata) + end +} + +local X509 = ffi.metatype('struct { X509 *cdata; }', mt) + +local function parse_pem_cert(str) + local bio = BIO.new() + + assert(bio:write(str)) + + return X509(C.PEM_read_bio_X509(bio.cdata, nil, nil, nil)) +end + +local function normalize_pem_cert(str) + if not str then return end + if #(str) == 0 then return end + + return re_gsub(str, [[\s(?!CERTIFICATE)]], '\n', 'oj') +end + +function _M.parse_pem_cert(str) + local crt = normalize_pem_cert(str) + + if crt then + return parse_pem_cert(crt) + else + return nil, 'invalid certificate' + end +end + +function _M:subject_name() + -- X509_get_subject_name() returns the subject name of certificate x. + -- The returned value is an internal pointer which MUST NOT be freed. + -- https://www.openssl.org/docs/man1.1.0/crypto/X509_get_subject_name.html + return X509_NAME.new(C.X509_get_subject_name(tocdata(self))) +end + +function _M:issuer_name() + -- X509_get_issuer_name() and X509_set_issuer_name() are identical to X509_get_subject_name() + -- and X509_set_subject_name() except the get and set the issuer name of x. + -- https://www.openssl.org/docs/man1.1.0/crypto/X509_get_subject_name.html + return X509_NAME.new(C.X509_get_issuer_name(tocdata(self))) +end + +function _M:digest(name) + local evp = EVP_MD.new(name) -- TODO: this EVP_MD object can he cached or passed + local md_size = #evp + local buf = ffi.new("unsigned char[?]", md_size) + local len = ffi.new("unsigned int[1]", md_size) + + ffi_assert(C.X509_digest(tocdata(self), tocdata(evp), buf, len), 1) + + return ffi.string(buf, len[0]) +end + +function _M:hexdigest(evp_md) + local digest = self:digest(evp_md) + + return resty_str.to_hex(digest) +end + +return _M diff --git a/resty/openssl/x509/name.lua b/resty/openssl/x509/name.lua new file mode 100644 index 0000000..42a99e9 --- /dev/null +++ b/resty/openssl/x509/name.lua @@ -0,0 +1,103 @@ +local base = require('resty.openssl.base') +local BIO = require('resty.openssl.bio') +local ffi = require('ffi') +local bit = require('bit') + +ffi.cdef([[ +int X509_NAME_print_ex(BIO *out, const X509_NAME *nm, int indent, unsigned long flags); +char * X509_NAME_oneline(const X509_NAME *a, char *buf, int size); +int X509_NAME_print(BIO *bp, const X509_NAME *name, int obase); +]]) + +local const = { + +} + +const.XN_FLAG_SEP_MASK = bit.lshift(0xf, 16) + +const.XN_FLAG_COMPAT = 0 +const.XN_FLAG_SEP_COMMA_PLUS = bit.lshift(1, 16) +const.XN_FLAG_SEP_CPLUS_SPC = bit.lshift(2, 16) +const.XN_FLAG_SEP_SPLUS_SPC = bit.lshift(3, 16) +const.XN_FLAG_SEP_MULTILINE = bit.lshift(4, 16) +const.XN_FLAG_DN_REV = bit.lshift(1, 20) +const.XN_FLAG_FN_MASK = bit.lshift(0x3, 21) +const.XN_FLAG_FN_SN = 0 +const.XN_FLAG_FN_LN = bit.lshift(1, 21) +const.XN_FLAG_FN_OID = bit.lshift(2, 21) +const.XN_FLAG_FN_NONE = bit.lshift(3, 21) +const.XN_FLAG_SPC_EQ = bit.lshift(1, 23) +const.XN_FLAG_DUMP_UNKNOWN_FIELDS = bit.lshift(1, 24) +const.XN_FLAG_FN_ALIGN = bit.lshift(1, 25) +const.ASN1_STRFLGS_ESC_2253 = 1 +const.ASN1_STRFLGS_ESC_CTRL = 2 +const.ASN1_STRFLGS_ESC_MSB = 4 +const.ASN1_STRFLGS_ESC_QUOTE = 8 +const.CHARTYPE_PRINTABLESTRING = 0x10 +const.CHARTYPE_FIRST_ESC_2253 = 0x20 +const.CHARTYPE_LAST_ESC_2253 = 0x40 +const.ASN1_STRFLGS_UTF8_CONVERT = 0x10 +const.ASN1_STRFLGS_IGNORE_TYPE = 0x20 +const.ASN1_STRFLGS_SHOW_TYPE = 0x40 +const.ASN1_STRFLGS_DUMP_ALL = 0x80 +const.ASN1_STRFLGS_DUMP_UNKNOWN = 0x100 +const.ASN1_STRFLGS_DUMP_DER = 0x200 +const.SN1_STRFLGS_ESC_2254 = 0x400 + +const.ASN1_STRFLGS_RFC2253 = bit.bor( + const.ASN1_STRFLGS_ESC_2253, + const.ASN1_STRFLGS_ESC_CTRL, + const.ASN1_STRFLGS_ESC_MSB, + const.ASN1_STRFLGS_UTF8_CONVERT, + const.ASN1_STRFLGS_DUMP_UNKNOWN, + const.ASN1_STRFLGS_DUMP_DER +) + +const.XN_FLAG_RFC2253 = bit.bor( + const.ASN1_STRFLGS_RFC2253, + const.XN_FLAG_SEP_COMMA_PLUS, + const.XN_FLAG_DN_REV, + const.XN_FLAG_FN_SN, + const.XN_FLAG_DUMP_UNKNOWN_FIELDS +) + +const.XN_FLAG_ONELINE = bit.bor( + const.ASN1_STRFLGS_RFC2253, + const.ASN1_STRFLGS_ESC_QUOTE, + const.XN_FLAG_SEP_CPLUS_SPC, + const.XN_FLAG_SPC_EQ, + const.XN_FLAG_FN_SN +) + +const.XN_FLAG_MULTILINE = bit.bor( + const.ASN1_STRFLGS_ESC_CTRL, + const.ASN1_STRFLGS_ESC_MSB, + const.XN_FLAG_SEP_MULTILINE, + const.XN_FLAG_SPC_EQ, + const.XN_FLAG_FN_LN, + const.XN_FLAG_FN_ALIGN +) + +local C = ffi.C +local tocdata = base.tocdata +local assert = assert +local _M = {} +local mt = { + __index = _M, + __new = ffi.new, + __tostring = function(self) + local bio = BIO.new() + + C.X509_NAME_print_ex(tocdata(bio), tocdata(self), 0, const.XN_FLAG_ONELINE) + + return bio:read() + end +} + +local X509_NAME = ffi.metatype('struct { X509_NAME *cdata; }', mt) + +function _M.new(name) + return X509_NAME(assert(name)) +end + +return _M diff --git a/resty/openssl/x509/store.lua b/resty/openssl/x509/store.lua new file mode 100644 index 0000000..66a22c0 --- /dev/null +++ b/resty/openssl/x509/store.lua @@ -0,0 +1,82 @@ +local base = require('resty.openssl.base') +local X509_STORE_CTX = require('resty.openssl.x509.store.ctx') +local ffi = require('ffi') + +ffi.cdef([[ +// https://www.openssl.org/docs/man1.1.0/crypto/X509_STORE_new.html +X509_STORE *X509_STORE_new(void); +void X509_STORE_free(X509_STORE *v); +int X509_STORE_lock(X509_STORE *v); +int X509_STORE_unlock(X509_STORE *v); +int X509_STORE_up_ref(X509_STORE *v); + +// https://www.openssl.org/docs/man1.1.1/man3/X509_STORE_add_cert.html +int X509_STORE_add_cert(X509_STORE *store, X509 *x); +int X509_STORE_add_crl(X509_STORE *ctx, X509_CRL *x); +int X509_STORE_set_depth(X509_STORE *store, int depth); +int X509_STORE_set_flags(X509_STORE *ctx, unsigned long flags); +int X509_STORE_set_purpose(X509_STORE *ctx, int purpose); +int X509_STORE_set_trust(X509_STORE *ctx, int trust); + +// https://www.openssl.org/docs/man1.1.0/crypto/X509_STORE_set1_param.html +int X509_STORE_set1_param(X509_STORE *store, X509_VERIFY_PARAM *pm); + +// https://www.openssl.org/docs/man1.1.0/crypto/X509_STORE_CTX_set0_param.html +X509_VERIFY_PARAM *X509_VERIFY_PARAM_new(void); +int X509_VERIFY_PARAM_set_flags(X509_VERIFY_PARAM *param, + unsigned long flags); +void X509_VERIFY_PARAM_free(X509_VERIFY_PARAM *param); + +// https://www.openssl.org/docs/man1.1.1/man3/X509_VERIFY_PARAM_set_depth.html +]]) + +local C = ffi.C +local ffi_assert = base.ffi_assert +local tocdata = base.tocdata + +local X509_V_FLAG_PARTIAL_CHAIN = 0x80000 + +local function X509_VERIFY_PARAM(flags) + local verify_param = ffi_assert(C.X509_VERIFY_PARAM_new()) + + -- https://www.openssl.org/docs/man1.1.0/crypto/X509_VERIFY_PARAM_get_depth.html#example + ffi_assert(C.X509_VERIFY_PARAM_set_flags(verify_param, flags)) + + return ffi.gc(verify_param, C.X509_VERIFY_PARAM_free) +end + +local _M = {} +local mt = { + __index = _M, + __new = function(ct) + local store = ffi_assert(C.X509_STORE_new()) + + -- enabling partial chains allows us to trust leaf certificates + local verify_param = X509_VERIFY_PARAM(X509_V_FLAG_PARTIAL_CHAIN) + ffi_assert(C.X509_STORE_set1_param(store, verify_param),1) + + return ffi.new(ct, store) + end, + __gc = function(self) + C.X509_STORE_free(self.cdata) + end +} + +-- no changes to the metamethods possible from this point +local X509_STORE = ffi.metatype('struct { X509_STORE *cdata; }', mt) + +function _M:add_cert(x509) + return ffi_assert(C.X509_STORE_add_cert(tocdata(self), tocdata(x509))) +end + +function _M:validate_cert(x509, chain) + local ctx = X509_STORE_CTX.new(self, x509, chain) + + return ctx:validate() +end + +function _M.new() + return X509_STORE() +end + +return _M diff --git a/resty/openssl/x509/store/ctx.lua b/resty/openssl/x509/store/ctx.lua new file mode 100644 index 0000000..5b5b5e3 --- /dev/null +++ b/resty/openssl/x509/store/ctx.lua @@ -0,0 +1,53 @@ +local base = require('resty.openssl.base') local ffi = require('ffi') + +ffi.cdef([[ +// https://www.openssl.org/docs/man1.1.0/crypto/X509_STORE_CTX_init.html +int X509_STORE_CTX_init(X509_STORE_CTX *ctx, X509_STORE *store, + X509 *x509, const OPENSSL_STACK *chain); +void X509_STORE_CTX_cleanup(X509_STORE_CTX *ctx); +void X509_STORE_CTX_free(X509_STORE_CTX *ctx); +void X509_STORE_CTX_set0_param(X509_STORE_CTX *ctx, X509_VERIFY_PARAM *param); +X509_VERIFY_PARAM *X509_STORE_CTX_get0_param(X509_STORE_CTX *ctx); + +int X509_verify_cert(X509_STORE_CTX *ctx); +int X509_STORE_CTX_get_error(X509_STORE_CTX *ctx); + +const char *X509_verify_cert_error_string(long n); +]]) + +local C = ffi.C +local ffi_assert = base.ffi_assert +local tocdata = base.tocdata +local setmetatable = setmetatable + +local _M = {} +local mt = { __index = _M } + +local function new_ctx() + local ctx = ffi_assert(C.X509_STORE_CTX_new()) + + return ffi.gc(ctx, C.X509_STORE_CTX_free) +end + +function _M:validate() + local ctx = new_ctx() + + ffi_assert(C.X509_STORE_CTX_init(ctx, tocdata(self.store), tocdata(self.x509), self.chain), 1) + + local ret = C.X509_verify_cert(ctx) + + if ret == 1 then + return true + else + return false, ffi.string(C.X509_verify_cert_error_string(C.X509_STORE_CTX_get_error(ctx))) + end +end + +-- this could be optimized by reusing the context between validations, +-- but it is way harder to make safe when there are exceptions + +function _M.new(store, x509, chain) + return setmetatable({ store = store, x509 = x509, chain = chain }, mt) +end + +return _M diff --git a/t/example.t b/t/example.t deleted file mode 100644 index 8e8d24d..0000000 --- a/t/example.t +++ /dev/null @@ -1,35 +0,0 @@ -BEGIN { - $ENV{TEST_NGINX_APICAST_BINARY} ||= 'rover exec apicast'; - $ENV{APICAST_POLICY_LOAD_PATH} = './policies'; -} - -use strict; -use warnings FATAL => 'all'; -use Test::APIcast::Blackbox 'no_plan'; - -run_tests(); - -__DATA__ - -=== TEST 1: example -The module does not crash without configuration. ---- configuration -{ - "services": [ - { - "proxy": { - "policy_chain": [ - { "name": "example", "version": "0.1" }, - { "name": "apicast.policy.echo", "configuration": { } } - ] - } - } - ] -} ---- request -GET /t ---- response_body -GET /t HTTP/1.1 ---- error_code: 200 ---- no_error_log -[error]