diff --git a/src/ngx_stream_lua_ssl_certby.c b/src/ngx_stream_lua_ssl_certby.c index 19a554d8..7b7d20d2 100644 --- a/src/ngx_stream_lua_ssl_certby.c +++ b/src/ngx_stream_lua_ssl_certby.c @@ -44,6 +44,7 @@ static u_char *ngx_stream_lua_log_ssl_cert_error(ngx_log_t *log, u_char *buf, size_t len); static ngx_int_t ngx_stream_lua_ssl_cert_by_chunk(lua_State *L, ngx_stream_lua_request_t *r); +static int ngx_stream_lua_is_grease_cipher(uint16_t cipher_id); ngx_int_t @@ -986,6 +987,80 @@ ngx_stream_lua_ffi_ssl_raw_client_addr(ngx_stream_lua_request_t *r, char **addr, } +static int +ngx_stream_lua_is_grease_cipher(uint16_t cipher_id) +{ + /* GREASE values follow pattern: 0x?A?A where ? can be any hex digit */ + /* and both ? must be the same */ + /* Check if both bytes follow ?A pattern and high nibbles match */ + return (cipher_id & 0x0F0F) == 0x0A0A; +} + + +int +ngx_stream_lua_ffi_req_shared_ssl_ciphers(ngx_stream_lua_request_t *r, + uint16_t *ciphers, uint16_t *nciphers, int filter_grease, char **err) +{ +#ifdef OPENSSL_IS_BORINGSSL + + *err = "BoringSSL is not supported for SSL cipher operations"; + return NGX_ERROR; + +#else + ngx_ssl_conn_t *ssl_conn; + STACK_OF(SSL_CIPHER) *sk, *ck; + int sn, cn, i, n; + uint16_t cipher; + + if (r == NULL || 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; + } + + sk = SSL_get1_supported_ciphers(ssl_conn); + ck = SSL_get_client_ciphers(ssl_conn); + sn = sk_SSL_CIPHER_num(sk); + cn = sk_SSL_CIPHER_num(ck); + + if (sn > *nciphers) { + *err = "buffer too small"; + *nciphers = 0; + sk_SSL_CIPHER_free(sk); + return NGX_ERROR; + } + + for (*nciphers = 0, i = 0; i < sn; i++) { + cipher = SSL_CIPHER_get_protocol_id(sk_SSL_CIPHER_value(sk, i)); + + /* Skip GREASE ciphers if filtering is enabled */ + if (filter_grease && ngx_stream_lua_is_grease_cipher(cipher)) { + continue; + } + + for (n = 0; n < cn; n++) { + if (SSL_CIPHER_get_protocol_id(sk_SSL_CIPHER_value(ck, n)) + == cipher) + { + ciphers[(*nciphers)++] = cipher; + break; + } + } + } + + sk_SSL_CIPHER_free(sk); + + return NGX_OK; +#endif + +} + + int ngx_stream_lua_ffi_cert_pem_to_der(const u_char *pem, size_t pem_len, u_char *der, char **err) diff --git a/t/140-ssl-c-api.t b/t/140-ssl-c-api.t index e52202cf..3401dace 100644 --- a/t/140-ssl-c-api.t +++ b/t/140-ssl-c-api.t @@ -72,6 +72,9 @@ ffi.cdef[[ int ngx_stream_lua_ffi_ssl_client_random(ngx_stream_lua_request_t *r, unsigned char *out, size_t *outlen, char **err); + int ngx_stream_lua_ffi_req_shared_ssl_ciphers(void *r, uint16_t *ciphers, + uint16_t *nciphers, int filter_grease, char **err); + ]] _EOC_ } @@ -1374,3 +1377,195 @@ SUCCESS --- no_error_log [error] [alert] + + + +=== TEST 14: Get supported ciphers +--- stream_config + server { + listen unix:$TEST_NGINX_HTML_DIR/nginx.sock ssl; + + ssl_certificate_by_lua_block { + collectgarbage() + require "defines" + local ffi = require "ffi" + local cjson = require "cjson.safe" + + local MAX_CIPHERS = 64 + local ciphers = ffi.new("uint16_t[?]", MAX_CIPHERS) + local nciphers = ffi.new("uint16_t[1]", MAX_CIPHERS) + local err = 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 ret = ffi.C.ngx_stream_lua_ffi_req_shared_ssl_ciphers(r, ciphers, nciphers, 0, err) + + if ret ~= 0 then + ngx.log(ngx.ERR, "error getting ciphers: ", ffi.string(err[0])) + else + local res = {} + for i = 0, nciphers[0] - 1 do + local cipher_id = string.format("%04x", ciphers[i]) + table.insert(res, cipher_id) + end + ngx.log(ngx.INFO, "supported ciphers: ", cjson.encode(res)) + end + } + + ssl_certificate ../../cert/test.crt; + ssl_certificate_key ../../cert/test.key; + ssl_protocols TLSv1.2; + ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384; + + return 'cipher test works!\n'; + } +--- stream_server_config + proxy_pass unix:$TEST_NGINX_HTML_DIR/nginx.sock; + proxy_ssl on; + proxy_ssl_session_reuse off; + proxy_ssl_protocols TLSv1.2; + proxy_ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256; +--- stream_response +cipher test works! +--- error_log +supported ciphers: ["c02f","c02b"] +--- no_error_log +[error] +[alert] + + + +=== TEST 15: Get supported ciphers with GREASE filtering +--- stream_config + server { + listen unix:$TEST_NGINX_HTML_DIR/nginx.sock ssl; + + ssl_certificate_by_lua_block { + collectgarbage() + require "defines" + local ffi = require "ffi" + local cjson = require "cjson.safe" + local MAX_CIPHERS = 64 + local ciphers = ffi.new("uint16_t[?]", MAX_CIPHERS) + local nciphers = ffi.new("uint16_t[1]", MAX_CIPHERS) + local err = 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 + + -- Test without GREASE filtering + local ret = ffi.C.ngx_stream_lua_ffi_req_shared_ssl_ciphers(r, ciphers, nciphers, 0, err) + local res_no_filter = {} + if ret == 0 then + for i = 0, nciphers[0] - 1 do + local cipher_id = string.format("%04x", ciphers[i]) + table.insert(res_no_filter, cipher_id) + end + end + + -- Reset buffer for next test + nciphers[0] = MAX_CIPHERS + + -- Test with GREASE filtering + local ret = ffi.C.ngx_stream_lua_ffi_req_shared_ssl_ciphers(r, ciphers, nciphers, 1, err) + local res_with_filter = {} + if ret == 0 then + for i = 0, nciphers[0] - 1 do + local cipher_id = string.format("%04x", ciphers[i]) + table.insert(res_with_filter, cipher_id) + end + end + + ngx.log(ngx.INFO, "without_filter: ", cjson.encode(res_no_filter)) + ngx.log(ngx.INFO, "with_filter: ", cjson.encode(res_with_filter)) + } + + ssl_certificate ../../cert/test.crt; + ssl_certificate_key ../../cert/test.key; + ssl_protocols TLSv1.2; + ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384; + + return 'grease filter test works!\n'; + } +--- stream_server_config + proxy_pass unix:$TEST_NGINX_HTML_DIR/nginx.sock; + proxy_ssl on; + proxy_ssl_certificate ../../cert/mtls_client.crt; + proxy_ssl_certificate_key ../../cert/mtls_client.key; + proxy_ssl_session_reuse off; + +--- stream_response +grease filter test works! +--- error_log +without_filter: +with_filter: +--- no_error_log +[error] +[alert] + + + +=== TEST 16: SSL cipher API error handling (no SSL) +--- stream_config + server { + listen unix:$TEST_NGINX_HTML_DIR/nginx.sock; + + content_by_lua_block { + require "defines" + local ffi = require "ffi" + + local MAX_CIPHERS = 64 + local ciphers = ffi.new("uint16_t[?]", MAX_CIPHERS) + local nciphers = ffi.new("uint16_t[1]", MAX_CIPHERS) + local err = 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 ret = ffi.C.ngx_stream_lua_ffi_req_shared_ssl_ciphers(r, ciphers, nciphers, 0, err) + + if ret ~= 0 then + ngx.say("error: ", ffi.string(err[0])) + else + ngx.say("unexpected success") + end + } + } +--- stream_server_config + content_by_lua_block { + local sock = ngx.socket.tcp() + sock:settimeout(2000) + + local ok, err = sock:connect("unix:$TEST_NGINX_HTML_DIR/nginx.sock") + if not ok then + ngx.log(ngx.ERR, "failed to connect: ", err) + return + end + + local line, err = sock:receive() + if not line then + ngx.log(ngx.ERR, "failed to receive: ", err) + return + end + + ngx.say("received: ", line) + + local ok, err = sock:close() + ngx.say("close: ", ok, " ", err) + } + +--- stream_response +received: error: bad request +close: 1 nil + +--- no_error_log +[error] +[alert]