diff --git a/src/ngx_http_lua_ssl_certby.c b/src/ngx_http_lua_ssl_certby.c index 84e309316a..c253242d5a 100644 --- a/src/ngx_http_lua_ssl_certby.c +++ b/src/ngx_http_lua_ssl_certby.c @@ -36,6 +36,8 @@ static u_char *ngx_http_lua_log_ssl_cert_error(ngx_log_t *log, u_char *buf, size_t len); static ngx_int_t ngx_http_lua_ssl_cert_by_chunk(lua_State *L, ngx_http_request_t *r); +static int ngx_http_lua_ssl_password_callback(char *buf, int size, int rwflag, + void *userdata); ngx_int_t @@ -557,6 +559,37 @@ ngx_http_lua_ffi_ssl_get_tls1_version(ngx_http_request_t *r, char **err) } +static int +ngx_http_lua_ssl_password_callback(char *buf, int size, int rwflag, + void *userdata) +{ + ngx_str_t *pwd = userdata; + + if (rwflag) { + ngx_log_error(NGX_LOG_ALERT, ngx_cycle->log, 0, + "ngx_http_lua_ssl_password_callback() " + "is called for encryption"); + return 0; + } + + if (pwd->len == 0) { + return 0; + } + + if (pwd->len > (size_t) size) { + ngx_log_error(NGX_LOG_ERR, ngx_cycle->log, 0, + "password is truncated to %d bytes", size); + + } else { + size = pwd->len; + } + + ngx_memcpy(buf, pwd->data, size); + + return size; +} + + int ngx_http_lua_ffi_ssl_clear_certs(ngx_http_request_t *r, char **err) { @@ -982,6 +1015,51 @@ ngx_http_lua_ffi_priv_key_pem_to_der(const u_char *pem, size_t pem_len, } +int +ngx_http_lua_ffi_priv_key_pem_to_der_with_password(const u_char *pem, + size_t pem_len, const u_char *pwd, size_t pwd_len, u_char *der, char **err) +{ + int len; + BIO *in; + EVP_PKEY *pkey; + ngx_str_t password; + + password.data = (u_char *) pwd; + password.len = pwd_len; + + in = BIO_new_mem_buf((char *) pem, (int) pem_len); + if (in == NULL) { + *err = "BIO_new_mem_buf() failed"; + ERR_clear_error(); + return NGX_ERROR; + } + + pkey = PEM_read_bio_PrivateKey(in, NULL, + ngx_http_lua_ssl_password_callback, + (void *) &password); + if (pkey == NULL) { + BIO_free(in); + *err = "PEM_read_bio_PrivateKey() failed"; + ERR_clear_error(); + return NGX_ERROR; + } + + BIO_free(in); + + len = i2d_PrivateKey(pkey, &der); + if (len < 0) { + EVP_PKEY_free(pkey); + *err = "i2d_PrivateKey() failed"; + ERR_clear_error(); + return NGX_ERROR; + } + + EVP_PKEY_free(pkey); + + return len; +} + + void * ngx_http_lua_ffi_parse_pem_cert(const u_char *pem, size_t pem_len, char **err) @@ -1102,6 +1180,40 @@ ngx_http_lua_ffi_parse_pem_priv_key(const u_char *pem, size_t pem_len, } +void * +ngx_http_lua_ffi_parse_pem_priv_key_with_password(const u_char *pem, + size_t pem_len, const u_char *pwd, size_t pwd_len, char **err) +{ + BIO *in; + EVP_PKEY *pkey; + ngx_str_t password; + + password.data = (u_char *) pwd; + password.len = pwd_len; + + in = BIO_new_mem_buf((char *) pem, (int) pem_len); + if (in == NULL) { + *err = "BIO_new_mem_buf() failed"; + ERR_clear_error(); + return NULL; + } + + pkey = PEM_read_bio_PrivateKey(in, NULL, + ngx_http_lua_ssl_password_callback, + (void *) &password); + if (pkey == NULL) { + *err = "PEM_read_bio_PrivateKey() failed"; + BIO_free(in); + ERR_clear_error(); + return NULL; + } + + BIO_free(in); + + return pkey; +} + + void ngx_http_lua_ffi_free_priv_key(void *cdata) { diff --git a/t/151-ssl-c-api-password.t b/t/151-ssl-c-api-password.t new file mode 100644 index 0000000000..a52d6a3670 --- /dev/null +++ b/t/151-ssl-c-api-password.t @@ -0,0 +1,584 @@ +# vim:set ft= ts=4 sw=4 et fdm=marker: + +use Test::Nginx::Socket::Lua; + +repeat_each(3); + +# All these tests need to have new openssl +my $NginxBinary = $ENV{'TEST_NGINX_BINARY'} || 'nginx'; +my $openssl_version = eval { `$NginxBinary -V 2>&1` }; + +if ($openssl_version =~ m/built with OpenSSL (0|1\.0\.(?:0|1[^\d]|2[a-d]).*)/) { + plan(skip_all => "too old OpenSSL, need 1.0.2e, was $1"); +} else { + plan tests => repeat_each() * (blocks() * 5 - 1); +} + +$ENV{TEST_NGINX_HTML_DIR} ||= html_dir(); + +log_level 'debug'; +no_long_string(); + +add_block_preprocessor(sub { + my $block = shift; + + if (!defined $block->user_files) { + $block->set_value("user_files", <<'_EOC_'); +>>> defines.lua +local ffi = require "ffi" + +ffi.cdef[[ + int ngx_http_lua_ffi_cert_pem_to_der(const unsigned char *pem, + size_t pem_len, unsigned char *der, char **err); + + int ngx_http_lua_ffi_priv_key_pem_to_der_with_password(const unsigned char *pem, size_t pem_len, + const unsigned char *pwd, size_t pwd_len, unsigned char *der, char **err); + + void *ngx_http_lua_ffi_parse_pem_priv_key_with_password(const unsigned char *pem, size_t pem_len, + const unsigned char*pwd, size_t pwd_len, char **err); + + int ngx_http_lua_ffi_ssl_clear_certs(void *r, char **err); + + int ngx_http_lua_ffi_ssl_set_der_certificate(void *r, + const char *data, size_t len, char **err); + + int ngx_http_lua_ffi_ssl_set_der_private_key(void *r, + const char *data, size_t len, char **err); + + void *ngx_http_lua_ffi_parse_pem_cert(const unsigned char *pem, + size_t pem_len, char **err); + + int ngx_http_lua_ffi_set_cert(void *r, + void *cdata, char **err); + + int ngx_http_lua_ffi_set_priv_key(void *r, + void *cdata, char **err); + + void ngx_http_lua_ffi_free_cert(void *cdata); + + void ngx_http_lua_ffi_free_priv_key(void *cdata); +]] +_EOC_ + } + + my $http_config = $block->http_config || ''; + $http_config .= <<'_EOC_'; +lua_package_path "$prefix/html/?.lua;;"; +_EOC_ + $block->set_value("http_config", $http_config); + + if (!defined $block->config) { + my $config = <<_EOC_; +server_tokens off; + +location /t { + content_by_lua_block { + do + 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.say("failed to connect: ", err) + return + end + + ngx.say("connected: ", ok) + + local sess, err = sock:sslhandshake(nil, "test.com", true) + if not sess then + ngx.say("failed to do SSL handshake: ", err) + return + end + + ngx.say("ssl handshake: ", type(sess)) + + local req = "GET /foo HTTP/1.0\\r\\nHost: test.com\\r\\nConnection: close\\r\\n\\r\\n" + local bytes, err = sock:send(req) + if not bytes then + ngx.say("failed to send http request: ", err) + return + end + + ngx.say("sent http request: ", bytes, " bytes.") + + while true do + local line, err = sock:receive() + if not line then + -- ngx.say("failed to receive response status line: ", err) + break + end + + ngx.say("received: ", line) + end + + local ok, err = sock:close() + ngx.say("close: ", ok, " ", err) + end -- do + -- collectgarbage() + } +} +_EOC_ + $block->set_value("config", $config); + } + + if (!defined $block->response_body) { + my $response_body = <<_EOC_; +connected: 1 +ssl handshake: userdata +sent http request: 56 bytes. +received: HTTP/1.1 201 Created +received: Server: nginx +received: Content-Type: text/plain +received: Content-Length: 4 +received: Connection: close +received: +received: foo +close: 1 nil +_EOC_ + $block->set_value("response_body", $response_body); + } +}); + +run_tests(); + +__DATA__ + +=== TEST 1: simple cert + private key with password +--- http_config + lua_ssl_trusted_certificate ../../cert/test.crt; + + server { + listen unix:$TEST_NGINX_HTML_DIR/nginx.sock ssl; + server_name test.com; + + ssl_certificate_by_lua_block { + collectgarbage() + + require "defines" + local ffi = require "ffi" + + local errmsg = ffi.new("char *[1]") + + local r = getfenv(0).__ngx_req + if not r then + ngx.log(ngx.ERR, "no request found") + return + end + + ffi.C.ngx_http_lua_ffi_ssl_clear_certs(r, errmsg) + + local f = assert(io.open("t/cert/test.crt", "rb")) + local cert = f:read("*all") + f:close() + + local out = ffi.new("char [?]", #cert) + + local rc = ffi.C.ngx_http_lua_ffi_cert_pem_to_der(cert, #cert, out, errmsg) + if rc < 1 then + ngx.log(ngx.ERR, "failed to parse PEM cert: ", + ffi.string(errmsg[0])) + return + end + + local cert_der = ffi.string(out, rc) + + local rc = ffi.C.ngx_http_lua_ffi_ssl_set_der_certificate(r, cert_der, #cert_der, errmsg) + if rc ~= 0 then + ngx.log(ngx.ERR, "failed to set DER cert: ", + ffi.string(errmsg[0])) + return + end + + f = assert(io.open("t/cert/test_with_password_openresty.key", "rb")) + local pkey = f:read("*all") + f:close() + + out = ffi.new("char [?]", #pkey) + + local pwd = "openresty" + local rc = ffi.C.ngx_http_lua_ffi_priv_key_pem_to_der_with_password( + pkey, #pkey, pwd, #pwd, out, errmsg) + if rc < 1 then + ngx.log(ngx.ERR, "failed to parse PEM priv key: ", + ffi.string(errmsg[0])) + return + end + + local pkey_der = ffi.string(out, rc) + + local rc = ffi.C.ngx_http_lua_ffi_ssl_set_der_private_key(r, pkey_der, #pkey_der, errmsg) + if rc ~= 0 then + ngx.log(ngx.ERR, "failed to set DER priv key: ", + ffi.string(errmsg[0])) + return + end + } + + ssl_certificate ../../cert/test2.crt; + ssl_certificate_key ../../cert/test2.key; + + server_tokens off; + location /foo { + default_type 'text/plain'; + content_by_lua_block { ngx.status = 201 ngx.say("foo") ngx.exit(201) } + more_clear_headers Date; + } + } + +--- request +GET /t + +--- error_log +lua ssl server name: "test.com" + +--- no_error_log +[error] +[alert] + + + +=== TEST 2: ECDSA cert + private key with password +--- http_config + lua_ssl_trusted_certificate ../../cert/test_ecdsa.crt; + + server { + listen unix:$TEST_NGINX_HTML_DIR/nginx.sock ssl; + server_name test.com; + ssl_certificate_by_lua_block { + collectgarbage() + + local ffi = require "ffi" + require "defines" + + local errmsg = ffi.new("char *[1]") + local r = getfenv(0).__ngx_req + if not r then + ngx.log(ngx.ERR, "no request found") + return + end + + ffi.C.ngx_http_lua_ffi_ssl_clear_certs(r, errmsg) + + local f = assert(io.open("t/cert/test_ecdsa.crt", "rb")) + local cert = f:read("*all") + f:close() + + local out = ffi.new("char [?]", #cert) + local rc = ffi.C.ngx_http_lua_ffi_cert_pem_to_der(cert, #cert, out, errmsg) + if rc < 1 then + ngx.log(ngx.ERR, "failed to parse PEM cert: ", + ffi.string(errmsg[0])) + return + end + + local cert_der = ffi.string(out, rc) + + local rc = ffi.C.ngx_http_lua_ffi_ssl_set_der_certificate(r, cert_der, #cert_der, errmsg) + if rc ~= 0 then + ngx.log(ngx.ERR, "failed to set DER cert: ", + ffi.string(errmsg[0])) + return + end + + f = assert(io.open("t/cert/test_ecdsa_with_password_openresty.key", "rb")) + local pkey = f:read("*all") + f:close() + + out = ffi.new("char [?]", #pkey) + + local pwd = "openresty" + local rc = ffi.C.ngx_http_lua_ffi_priv_key_pem_to_der_with_password( + pkey, #pkey, pwd, #pwd, out, errmsg) + if rc < 1 then + ngx.log(ngx.ERR, "failed to parse PEM priv key: ", + ffi.string(errmsg[0])) + return + end + + local pkey_der = ffi.string(out, rc) + + local rc = ffi.C.ngx_http_lua_ffi_ssl_set_der_private_key(r, pkey_der, #pkey_der, errmsg) + if rc ~= 0 then + ngx.log(ngx.ERR, "failed to set DER priv key: ", + ffi.string(errmsg[0])) + return + end + } + + ssl_certificate ../../cert/test2.crt; + ssl_certificate_key ../../cert/test2.key; + + server_tokens off; + + location /foo { + default_type 'text/plain'; + content_by_lua_block { ngx.status = 201 ngx.say("foo") ngx.exit(201) } + more_clear_headers Date; + } + } + +--- request +GET /t + +--- error_log +lua ssl server name: "test.com" + +--- no_error_log +[error] +[alert] + + + +=== TEST 3: simple cert + private key cdata with password +--- http_config + lua_ssl_trusted_certificate ../../cert/test.crt; + + server { + listen unix:$TEST_NGINX_HTML_DIR/nginx.sock ssl; + server_name test.com; + ssl_certificate_by_lua_block { + collectgarbage() + + local ffi = require "ffi" + require "defines" + + local errmsg = ffi.new("char *[1]") + + local r = getfenv(0).__ngx_req + if not r then + ngx.log(ngx.ERR, "no request found") + return + end + + ffi.C.ngx_http_lua_ffi_ssl_clear_certs(r, errmsg) + + local f = assert(io.open("t/cert/test.crt", "rb")) + local cert_data = f:read("*all") + f:close() + + local cert = ffi.C.ngx_http_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_http_lua_ffi_set_cert(r, cert, errmsg) + if rc ~= 0 then + ngx.log(ngx.ERR, "failed to set cdata cert: ", + ffi.string(errmsg[0])) + return + end + + ffi.C.ngx_http_lua_ffi_free_cert(cert) + + f = assert(io.open("t/cert/test_with_password_openresty.key", "rb")) + local pkey_data = f:read("*all") + f:close() + + local pwd = "openresty" + + local pkey = ffi.C.ngx_http_lua_ffi_parse_pem_priv_key_with_password( + pkey_data, #pkey_data, pwd, #pwd, errmsg) + if pkey == nil then + ngx.log(ngx.ERR, "failed to parse PEM priv key: ", + ffi.string(errmsg[0])) + return + end + + local rc = ffi.C.ngx_http_lua_ffi_set_priv_key(r, pkey, errmsg) + if rc ~= 0 then + ngx.log(ngx.ERR, "failed to set cdata priv key: ", + ffi.string(errmsg[0])) + return + end + + ffi.C.ngx_http_lua_ffi_free_priv_key(pkey) + } + + ssl_certificate ../../cert/test2.crt; + ssl_certificate_key ../../cert/test2.key; + + server_tokens off; + + location /foo { + default_type 'text/plain'; + content_by_lua_block { ngx.status = 201 ngx.say("foo") ngx.exit(201) } + more_clear_headers Date; + } + } + +--- request +GET /t + +--- error_log +lua ssl server name: "test.com" + +--- no_error_log +[error] +[alert] + + + +=== TEST 4: ECDSA cert + private key cdata with password +--- http_config + lua_ssl_trusted_certificate ../../cert/test_ecdsa.crt; + + server { + listen unix:$TEST_NGINX_HTML_DIR/nginx.sock ssl; + server_name test.com; + + ssl_certificate_by_lua_block { + collectgarbage() + + local ffi = require "ffi" + require "defines" + + local errmsg = ffi.new("char *[1]") + + local r = getfenv(0).__ngx_req + if not r then + ngx.log(ngx.ERR, "no request found") + return + end + + ffi.C.ngx_http_lua_ffi_ssl_clear_certs(r, errmsg) + + local f = assert(io.open("t/cert/test_ecdsa.crt", "rb")) + local cert_data = f:read("*all") + f:close() + + local cert = ffi.C.ngx_http_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_http_lua_ffi_set_cert(r, cert, errmsg) + if rc ~= 0 then + ngx.log(ngx.ERR, "failed to set cdata cert: ", + ffi.string(errmsg[0])) + return + end + + ffi.C.ngx_http_lua_ffi_free_cert(cert) + + f = assert(io.open("t/cert/test_ecdsa_with_password_openresty.key", "rb")) + local pkey_data = f:read("*all") + f:close() + + local pwd = "openresty" + local pkey = ffi.C.ngx_http_lua_ffi_parse_pem_priv_key_with_password( + pkey_data, #pkey_data, pwd, #pwd, errmsg) + if pkey == nil then + ngx.log(ngx.ERR, "failed to parse PEM priv key: ", + ffi.string(errmsg[0])) + return + end + + local rc = ffi.C.ngx_http_lua_ffi_set_priv_key(r, pkey, errmsg) + if rc ~= 0 then + ngx.log(ngx.ERR, "failed to set cdata priv key: ", + ffi.string(errmsg[0])) + return + end + + ffi.C.ngx_http_lua_ffi_free_priv_key(pkey) + } + + ssl_certificate ../../cert/test2.crt; + ssl_certificate_key ../../cert/test2.key; + + server_tokens off; + + location /foo { + default_type 'text/plain'; + content_by_lua_block { ngx.status = 201 ngx.say("foo") ngx.exit(201) } + more_clear_headers Date; + } + } + +--- request +GET /t + +--- error_log +lua ssl server name: "test.com" + +--- no_error_log +[error] +[alert] + + + +=== TEST 5: ffi private key with password +--- config + server_tokens off; + + location /t { + content_by_lua_block { + local ffi = require "ffi" + require "defines" + + f = assert(io.open("t/cert/test_with_password_openresty.key", "rb")) + local pkey_data = f:read("*all") + f:close() + + local errmsg = ffi.new("char *[1]") + + local pwd = "openresty" + + local pkey = ffi.C.ngx_http_lua_ffi_parse_pem_priv_key_with_password(pkey_data, #pkey_data, pwd, #pwd, errmsg) + if pkey == nil then + ngx.say("parse key with right password: error") + else + ngx.say("parse key with right password: success") + end + + pwd = "wrongpassword" + pkey = ffi.C.ngx_http_lua_ffi_parse_pem_priv_key_with_password(pkey_data, #pkey_data, pwd, #pwd, errmsg) + if pkey == nil then + ngx.log(ngx.ERR, "failed to parse PEM priv key with wrong password: ", + ffi.string(errmsg[0])) + ngx.say("parse key with wrong password: success") + else + ngx.say("parse key with right password: error") + end + + local cert = pkey_data + local out = ffi.new("char [?]", #cert) + + pwd = "openresty" + local rc = ffi.C.ngx_http_lua_ffi_priv_key_pem_to_der_with_password(cert, #cert, pwd, #pwd, out, errmsg) + if rc < 1 then + ngx.say("pem to der with right password: error") + else + ngx.say("pem to der with right password: success") + end + + pwd = "wrongpassword" + local rc = ffi.C.ngx_http_lua_ffi_priv_key_pem_to_der_with_password(cert, #cert, pwd, #pwd, out, errmsg) + if rc < 1 then + ngx.log(ngx.ERR, "failed to transform PEM to DER with wrong password: ", + ffi.string(errmsg[0])) + ngx.say("pem to der with wrong password: success") + else + ngx.say("pem to der with wrong password: error") + end + } + } + +--- request +GET /t +--- response_body +parse key with right password: success +parse key with wrong password: success +pem to der with right password: success +pem to der with wrong password: success + +--- error_log +failed to parse PEM priv key with wrong password: PEM_read_bio_PrivateKey +failed to transform PEM to DER with wrong password: PEM_read_bio_PrivateKey + diff --git a/t/cert/test_ecdsa_with_password_openresty.key b/t/cert/test_ecdsa_with_password_openresty.key new file mode 100644 index 0000000000..1b7d1ef78c --- /dev/null +++ b/t/cert/test_ecdsa_with_password_openresty.key @@ -0,0 +1,8 @@ +-----BEGIN EC PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: DES-EDE3-CBC,E10D3514C833E7DB + +i14NkITdIEUw16Bs9tWKKgdFbgVrOda2lNTuo+wEsetKUkeGN/+c+1ocEy3wnGoc +tQJjATOML+zn0FK3DjnnamjrTSmuI2T4cCwNsMdfVmxajaZ/ru8NG9j4R5yOnfb0 +5dTJaOimLR9rLatazNyBX5EySaxdV6qcqjueLxQSvjs= +-----END EC PRIVATE KEY----- diff --git a/t/cert/test_with_password_openresty.key b/t/cert/test_with_password_openresty.key new file mode 100644 index 0000000000..6d366fe7c0 --- /dev/null +++ b/t/cert/test_with_password_openresty.key @@ -0,0 +1,18 @@ +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: DES-EDE3-CBC,1B6334AB2218970F + +bGEgmiXQS+UcYFQ6E7y7HyIg/W6ZHmrIKJRdGpq1jWSGiBAZQq5xMI4IzHywhnfi +uv6ZQ4PcbxZs5fRAR6jtH0Pewak6XCYYEMHp7xbuvo5dO05yh5gXJmYpxaJ4twjB +PVan6UkTTT/nzis3Auo2YZOqW0W+J+ZYVrzl905BWsslaK+GGYBIYfnJgrVtPTbo +uo6kfLDAf1n90AejdbATWY83USOgRufWi1/IMz7kFSR8f3QUJd2tjutHb79VTW/6 +OFq4L1hDOeL2NpPRcwUS3PjSJD9Sar33mbdZPZagBivZcwyxh0lMOst7fB+pLD+v +RLYHUCdT6akFgAuu2/Hs+xjHtPQBnaPvn1C/802Wpbh3ulDmn3UCaOBzm1RD8yw2 +XWna2HlefjnIdcTkQom0gcPSRUmCo8//0oF1GGmJM+A+3vrg6tLvZqZwoO+kU4s1 +3x1B1zNl2jSMpZRoUlHFfOVh+FdAITMaihHQjQ6OmxXEhTTHPAiy2gRkFWKmKQFw +4tupvGHaLA1/rjiMzu1lwvZ8OD0UmJG1GlLqHVVJuZ/NC5SvnuBI+AUQS9mALPTB ++PyOhDIa3hIvpzICbNLxaG58nOwySZqeWWMvpvpd9SU+Ui7JCOz6picyEzzWTVtz +Xwhrx2Hw0ghenEwdsaidECIPI/pf82wdPMkqmaMkI3y11YQXMHJoTvRybRYHYZzg +pkxjmnOflrvX1PWb2ZiOZf0k6fA9nYAyM3xsSR74qNxVKibo0GIxPop3Z/7imJZq +4TV+yPHEM8VpdJjubhkEAp3cmZ6cuXjorXmCCqO/MZCnFXaV7aMWug== +-----END RSA PRIVATE KEY-----