diff --git a/README.md b/README.md index c60dd88..7419f47 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,12 @@ Download lua-resty-openssl make openssl ``` +Install lua module + +``` +make install +``` + ## Examples: **test/mtls.conf:** diff --git a/lib/resty/tls.lua b/lib/resty/tls.lua index 2369f09..e6b5b89 100644 --- a/lib/resty/tls.lua +++ b/lib/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 @@ -89,4 +100,29 @@ function _M.set_upstream_ssl_verify(verify, verify_depth) 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 diff --git a/src/ngx_http_apicast_module.c b/src/ngx_http_apicast_module.c index 5df447a..270cb08 100644 --- a/src/ngx_http_apicast_module.c +++ b/src/ngx_http_apicast_module.c @@ -90,7 +90,8 @@ ngx_http_apicast_set_ctx(ngx_http_request_t *r) } -static ngx_int_t ngx_http_apicast_handler(ngx_http_request_t *r) +static ngx_int_t +ngx_http_apicast_handler(ngx_http_request_t *r) { /* @TODO validate here that the ctx is deleted and it's not leaking */ ngx_http_apicast_set_ctx(r); @@ -98,7 +99,93 @@ static ngx_int_t ngx_http_apicast_handler(ngx_http_request_t *r) } -int ngx_http_apicast_ffi_set_proxy_cert_key( +int +ngx_http_apicast_ffi_get_full_client_certificate_chain(ngx_http_request_t *r, + u_char **value, size_t *value_len) +{ + ngx_connection_t *c; + X509 *cert; + STACK_OF(X509) *chain; + BIO *bio; + int i, numcerts; + size_t len; + ngx_str_t s; + + if (r->connection == NULL || r->connection->ssl == NULL) { + return NGX_ERROR; + } + + c = r->connection; + if (c == NULL) { + return NGX_ERROR; + } + + s.len = 0; + + // First get peer's certificate + // See: https://docs.openssl.org/1.1.1/man3/SSL_get_peer_cert_chain + cert = SSL_get_peer_certificate(c->ssl->connection); + if (cert == NULL) { + /* client did not present a certificate or server did not request it */ + return NGX_DECLINED; + } + + bio = BIO_new(BIO_s_mem()); + if(!bio) { + ngx_ssl_error(NGX_LOG_ERR, c->log, 0, "BIO_new() failed"); + X509_free(cert); + return NGX_ERROR; + } + + if (PEM_write_bio_X509(bio, cert) == 0) { + ngx_ssl_error(NGX_LOG_ERR, c->log, 0, "PEM_write_bio_X509() failed"); + X509_free(cert); + goto failed; + } + + X509_free(cert); + + chain = SSL_get_peer_cert_chain(c->ssl->connection); + if(!chain) { + ngx_ssl_error(NGX_LOG_ERR, c->log, 0, "SSL_get_peer_cert_chain() failed"); + goto failed; + } + + numcerts = sk_X509_num(chain); + + for(i = 0; i < numcerts; i++) { + cert = sk_X509_value(chain, i); + + if (PEM_write_bio_X509(bio, cert) == 0) { + ngx_ssl_error(NGX_LOG_ERR, c->log, 0, "PEM_write_bio_X509() failed"); + goto failed; + } + } + + len = BIO_pending(bio); + s.len = len; + s.data = ngx_pnalloc(r->pool, len); + if (s.data == NULL) { + goto failed; + } + + BIO_read(bio, s.data, len); + *value = s.data; + *value_len = s.len; + + BIO_free(bio); + return NGX_OK; + +failed: + + BIO_free(bio); + + return NGX_ERROR; +} + + +int +ngx_http_apicast_ffi_set_proxy_cert_key( ngx_http_request_t *r, void *cdata_chain, void *cdata_key) { char *err = ""; @@ -227,7 +314,8 @@ ngx_http_apicast_ffi_set_ssl_verify(ngx_http_request_t *r, int verify, } -ngx_int_t ngx_http_apicast_set_proxy_cert_if_set( +ngx_int_t +ngx_http_apicast_set_proxy_cert_if_set( ngx_http_apicast_ctx_t *ctx, ngx_connection_t *conn) { char *err = ""; @@ -274,7 +362,8 @@ ngx_int_t ngx_http_apicast_set_proxy_cert_if_set( } -ngx_int_t ngx_http_apicast_set_proxy_cert_key_if_set( +ngx_int_t +ngx_http_apicast_set_proxy_cert_key_if_set( ngx_http_apicast_ctx_t *ctx, ngx_connection_t *conn) { @@ -306,7 +395,8 @@ ngx_int_t ngx_http_apicast_set_proxy_cert_key_if_set( } -ngx_int_t ngx_http_apicast_set_proxy_ca_cert_if_set( +ngx_int_t +ngx_http_apicast_set_proxy_ca_cert_if_set( ngx_http_apicast_ctx_t *ctx, ngx_connection_t *conn) { @@ -332,7 +422,8 @@ ngx_int_t ngx_http_apicast_set_proxy_ca_cert_if_set( } -ngx_int_t ngx_http_apicast_set_proxy_ssl_verify(ngx_http_apicast_ctx_t *ctx, +ngx_int_t +ngx_http_apicast_set_proxy_ssl_verify(ngx_http_apicast_ctx_t *ctx, ngx_connection_t *conn) { if (ctx == NULL) { diff --git a/t/001-upstream-mtls.t b/t/001-upstream-mtls.t index 588f0c1..ee84c97 100644 --- a/t/001-upstream-mtls.t +++ b/t/001-upstream-mtls.t @@ -7,6 +7,17 @@ my $pwd = cwd(); $ENV{TEST_NGINX_HTML_DIR} ||= html_dir(); +sub read_file { + my $infile = shift; + open my $in, $infile + or die "cannot open $infile for reading: $!"; + my $cert = do { local $/; <$in> }; + close $in; + $cert; +} + +our $ClientCertChain = read_file('t/fixtures/client_chain.crt'); + log_level 'debug'; # no_long_string(); @@ -90,7 +101,7 @@ client sent no required SSL certificate while reading client request headers server_tokens off; location /t { access_by_lua_block { - mtls = require("resty.mtls") + mtls = require("resty.tls") local ssl = require("ngx.ssl") local f = assert(io.open("t/fixtures/client_chain.crt")) local cert = f:read("*a") @@ -158,7 +169,7 @@ verify:1, error:0, depth:0, subject:"/CN=test", issuer:"/CN=sub.ca" server_tokens off; location /t { access_by_lua_block { - mtls = require("resty.mtls") + mtls = require("resty.tls") local ssl = require("ngx.ssl") local f = assert(io.open("t/fixtures/client_chain.crt")) local cert = f:read("*a") @@ -224,7 +235,7 @@ verify:1, error:0, depth:0, subject:"/CN=test", issuer:"/CN=sub.ca" server_tokens off; location /t { access_by_lua_block { - mtls = require("resty.mtls") + mtls = require("resty.tls") local ssl = require("ngx.ssl") local f = assert(io.open("t/fixtures/client_chain.crt")) local cert = f:read("*a") @@ -287,7 +298,7 @@ upstream SSL certificate verify error: (21:unable to verify the first certificat server_tokens off; location /t { access_by_lua_block { - mtls = require("resty.mtls") + mtls = require("resty.tls") local ssl = require("ngx.ssl") local f = assert(io.open("t/fixtures/client_chain.crt")) local cert = f:read("*a") @@ -350,7 +361,7 @@ SSL_do_handshake() failed server_tokens off; location /t { access_by_lua_block { - mtls = require("resty.mtls") + mtls = require("resty.tls") local ssl = require("ngx.ssl") local f = assert(io.open("t/fixtures/client_chain.crt")) local cert = f:read("*a") @@ -413,7 +424,7 @@ SSL_do_handshake() failed server_tokens off; location /t { access_by_lua_block { - mtls = require("resty.mtls") + mtls = require("resty.tls") local ssl = require("ngx.ssl") local f = assert(io.open("t/fixtures/client_chain.crt")) local cert = f:read("*a") @@ -476,7 +487,7 @@ yay, API backend server_tokens off; location /t { access_by_lua_block { - mtls = require("resty.mtls") + mtls = require("resty.tls") local ssl = require("ngx.ssl") local f = assert(io.open("t/fixtures/client_chain.crt")) local cert = f:read("*a") @@ -557,7 +568,7 @@ X509_check_host(): match server_tokens off; location /t { access_by_lua_block { - mtls = require("resty.mtls") + mtls = require("resty.tls") local ssl = require("ngx.ssl") local f = assert(io.open("t/fixtures/client_chain.crt")) local cert = f:read("*a") @@ -599,3 +610,96 @@ SSL_do_handshake() failed --- no_error_log [error] [alert] + + + +=== TEST 10: get_full_client_certificate_chain behaves normally +--- http_config + lua_package_path "../lua-resty-core/lib/?.lua;lualib/?.lua;;"; + + server { + listen unix:$TEST_NGINX_HTML_DIR/nginx.sock ssl; + server_name example.com; + ssl_certificate ../../fixtures/example.com.crt; + ssl_certificate_key ../../fixtures/example.com.key; + ssl_client_certificate ../../fixtures/rootCA.pem; + ssl_verify_client on; + + server_tokens off; + location /foo { + default_type 'text/plain'; + content_by_lua_block { + local chain = require("resty.tls").get_full_client_certificate_chain() + if chain then + chain = chain:sub(0, -2) + end + ngx.say(chain) + } + } + } +--- config + server_tokens off; + + location /t { + proxy_ssl_name example.com; + proxy_ssl_session_reuse off; + proxy_ssl_certificate ../../fixtures/client_chain.crt; + proxy_ssl_certificate_key ../../fixtures/client.key; + proxy_pass https://unix:$TEST_NGINX_HTML_DIR/nginx.sock:/foo; + proxy_ssl_server_name on; + } +--- request +GET /t +--- response_body eval +$::ClientCertChain +--- no_error_log +[error] +[alert] +[warn] +[crit] + + + +=== TEST 11: get_full_client_certificate_chain returns nil when no client certificates +is provided +--- http_config + lua_package_path "../lua-resty-core/lib/?.lua;lualib/?.lua;;"; + + server { + listen unix:$TEST_NGINX_HTML_DIR/nginx.sock ssl; + server_name example.com; + ssl_certificate ../../fixtures/example.com.crt; + ssl_certificate_key ../../fixtures/example.com.key; + + server_tokens off; + location /foo { + default_type 'text/plain'; + content_by_lua_block { + local chain = require("resty.tls").get_full_client_certificate_chain() + if chain then + chain = chain:sub(0, -2) + end + ngx.say(chain) + } + } + } +--- config + server_tokens off; + + location /t { + proxy_ssl_name example.com; + proxy_ssl_session_reuse off; + proxy_ssl_certificate ../../fixtures/client_chain.crt; + proxy_ssl_certificate_key ../../fixtures/client.key; + proxy_pass https://unix:$TEST_NGINX_HTML_DIR/nginx.sock:/foo; + proxy_ssl_server_name on; + } +--- request +GET /t +--- response_body +nil +--- no_error_log +[error] +[alert] +[warn] +[crit]