Skip to content

feature: add FFI interface to verify SSL client certificate #190

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

Merged
merged 10 commits into from
Aug 3, 2020
Merged
138 changes: 138 additions & 0 deletions src/ngx_stream_lua_ssl_certby.c
Original file line number Diff line number Diff line change
Expand Up @@ -1317,4 +1317,142 @@ ngx_stream_lua_ffi_set_priv_key(ngx_stream_lua_request_t *r,
}


static int
ngx_stream_lua_ssl_verify_callback(int ok, X509_STORE_CTX *x509_store)
{
/*
* we never terminate handshake here and user can later use
* $ssl_client_verify to check verification result.
*
* this is consistent with Nginx behavior.
*/
return 1;
}


int
ngx_stream_lua_ffi_ssl_verify_client(ngx_stream_lua_request_t *r,
void *ca_certs, int depth, char **err)
{
ngx_stream_lua_ctx_t *ctx;
ngx_ssl_conn_t *ssl_conn;
ngx_stream_ssl_conf_t *sscf;
STACK_OF(X509) *chain = ca_certs;
STACK_OF(X509_NAME) *name_chain = NULL;
X509 *x509 = NULL;
X509_NAME *subject = NULL;
X509_STORE *ca_store = NULL;
#ifdef OPENSSL_IS_BORINGSSL
size_t i;
#else
int i;
#endif

ctx = ngx_stream_get_module_ctx(r->session, ngx_stream_lua_module);
if (ctx == NULL) {
*err = "no request ctx found";
return NGX_ERROR;
}

if (!(ctx->context & NGX_STREAM_LUA_CONTEXT_SSL_CERT)) {
*err = "API disabled in the current context";
return NGX_ERROR;
}

if (r->connection == NULL || r->connection->ssl == NULL) {
*err = "bad request";
return NGX_ERROR;
}

ssl_conn = r->connection->ssl->connection;
if (ssl_conn == NULL) {
*err = "bad ssl conn";
return NGX_ERROR;
}

/* enable verify */

SSL_set_verify(ssl_conn, SSL_VERIFY_PEER,
ngx_stream_lua_ssl_verify_callback);

/* set depth */

if (depth < 0) {
sscf = ngx_stream_get_module_srv_conf(r->session,
ngx_stream_ssl_module);
if (sscf != NULL) {
depth = sscf->verify_depth;

} else {
/* same as the default value of ssl_verify_depth */
depth = 1;
}
}

SSL_set_verify_depth(ssl_conn, depth);

/* set CA chain */

if (chain != NULL) {
ca_store = X509_STORE_new();
if (ca_store == NULL) {
*err = "X509_STORE_new() failed";
return NGX_ERROR;
}

/* construct name chain */

name_chain = sk_X509_NAME_new_null();
if (name_chain == NULL) {
*err = "sk_X509_NAME_new_null() failed";
goto failed;
}

for (i = 0; i < sk_X509_num(chain); i++) {
x509 = sk_X509_value(chain, i);
if (x509 == NULL) {
*err = "sk_X509_value() failed";
goto failed;
}

/* add subject to name chain, which will be sent to client */
subject = X509_NAME_dup(X509_get_subject_name(x509));
if (subject == NULL) {
*err = "X509_get_subject_name() failed";
goto failed;
}

if (!sk_X509_NAME_push(name_chain, subject)) {
*err = "sk_X509_NAME_push() failed";
X509_NAME_free(subject);
goto failed;
}

/* add to trusted CA store */
if (X509_STORE_add_cert(ca_store, x509) == 0) {
*err = "X509_STORE_add_cert() failed";
goto failed;
}
}

if (SSL_set0_verify_cert_store(ssl_conn, ca_store) == 0) {
*err = "SSL_set0_verify_cert_store() failed";
goto failed;
}

SSL_set_client_CA_list(ssl_conn, name_chain);
}

return NGX_OK;

failed:

sk_X509_NAME_free(name_chain);

X509_STORE_free(ca_store);

return NGX_ERROR;
}


#endif /* NGX_STREAM_SSL */
189 changes: 189 additions & 0 deletions t/140-ssl-c-api.t
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ ffi.cdef[[
void ngx_stream_lua_ffi_free_priv_key(void *cdata);

int ngx_stream_lua_ffi_ssl_clear_certs(void *r, char **err);

int ngx_stream_lua_ffi_ssl_verify_client(void *r, void *cdata, int depth, char **err);

]]
_EOC_
}
Expand Down Expand Up @@ -675,3 +678,189 @@ lua ssl server name: "test.com"
--- no_error_log
[error]
[alert]



=== TEST 6: verify client with CA certificates
--- stream_config
server {
listen unix:$TEST_NGINX_HTML_DIR/nginx.sock ssl;

ssl_certificate ../../cert/test2.crt;
ssl_certificate_key ../../cert/test2.key;

ssl_certificate_by_lua_block {
collectgarbage()

local ffi = require "ffi"
require "defines"

local errmsg = ffi.new("char *[1]")

local r = require "resty.core.base" .get_request()
if not r then
ngx.log(ngx.ERR, "no request found")
return
end

local f = assert(io.open("t/cert/test.crt", "rb"))
local cert_data = f:read("*all")
f:close()

local cert = ffi.C.ngx_stream_lua_ffi_parse_pem_cert(cert_data, #cert_data, errmsg)
if not cert then
ngx.log(ngx.ERR, "failed to parse PEM cert: ",
ffi.string(errmsg[0]))
return
end

local rc = ffi.C.ngx_stream_lua_ffi_ssl_verify_client(r, cert, -1, errmsg)
if rc ~= 0 then
ngx.log(ngx.ERR, "failed to set cdata cert: ",
ffi.string(errmsg[0]))
return
end

ffi.C.ngx_stream_lua_ffi_free_cert(cert)
}

content_by_lua_block {
print('client certificate subject: ', ngx.var.ssl_client_s_dn)
ngx.say(ngx.var.ssl_client_verify)
}
}
--- stream_server_config
proxy_pass unix:$TEST_NGINX_HTML_DIR/nginx.sock;
proxy_ssl on;
proxy_ssl_certificate ../../cert/test.crt;
proxy_ssl_certificate_key ../../cert/test.key;
proxy_ssl_session_reuse off;

--- stream_response
SUCCESS

--- error_log
client certificate subject: [email protected],CN=test.com

--- no_error_log
[error]
[alert]



=== TEST 7: verify client without CA certificates
--- stream_config
server {
listen unix:$TEST_NGINX_HTML_DIR/nginx.sock ssl;

ssl_certificate ../../cert/test2.crt;
ssl_certificate_key ../../cert/test2.key;

ssl_certificate_by_lua_block {
collectgarbage()

local ffi = require "ffi"
require "defines"

local errmsg = ffi.new("char *[1]")

local r = require "resty.core.base" .get_request()
if not r then
ngx.log(ngx.ERR, "no request found")
return
end

local rc = ffi.C.ngx_stream_lua_ffi_ssl_verify_client(r, nil, -1, errmsg)
if rc ~= 0 then
ngx.log(ngx.ERR, "failed to set cdata cert: ",
ffi.string(errmsg[0]))
return
end
}

content_by_lua_block {
print('client certificate subject: ', ngx.var.ssl_client_s_dn)
ngx.say(ngx.var.ssl_client_verify)
}
}
--- stream_server_config
proxy_pass unix:$TEST_NGINX_HTML_DIR/nginx.sock;
proxy_ssl on;
proxy_ssl_certificate ../../cert/test.crt;
proxy_ssl_certificate_key ../../cert/test.key;
proxy_ssl_session_reuse off;

--- stream_response
FAILED:self signed certificate

--- error_log
client certificate subject: [email protected],CN=test.com

--- no_error_log
[error]
[alert]



=== TEST 8: verify client but client provides no certificate
--- stream_config
server {
listen unix:$TEST_NGINX_HTML_DIR/nginx.sock ssl;

ssl_certificate ../../cert/test2.crt;
ssl_certificate_key ../../cert/test2.key;

ssl_certificate_by_lua_block {
collectgarbage()

local ffi = require "ffi"
require "defines"

local errmsg = ffi.new("char *[1]")

local r = require "resty.core.base" .get_request()
if not r then
ngx.log(ngx.ERR, "no request found")
return
end

local f = assert(io.open("t/cert/test.crt", "rb"))
local cert_data = f:read("*all")
f:close()

local cert = ffi.C.ngx_stream_lua_ffi_parse_pem_cert(cert_data, #cert_data, errmsg)
if not cert then
ngx.log(ngx.ERR, "failed to parse PEM cert: ",
ffi.string(errmsg[0]))
return
end

local rc = ffi.C.ngx_stream_lua_ffi_ssl_verify_client(r, cert, 1, errmsg)
if rc ~= 0 then
ngx.log(ngx.ERR, "failed to set cdata cert: ",
ffi.string(errmsg[0]))
return
end

ffi.C.ngx_stream_lua_ffi_free_cert(cert)
}

content_by_lua_block {
print('client certificate subject: ', ngx.var.ssl_client_s_dn)
ngx.say(ngx.var.ssl_client_verify)
}
}
--- stream_server_config
proxy_pass unix:$TEST_NGINX_HTML_DIR/nginx.sock;
proxy_ssl on;
proxy_ssl_session_reuse off;

--- stream_response
NONE

--- error_log
client certificate subject: nil

--- no_error_log
[error]
[alert]