barbieri pushed a commit to branch master. http://git.enlightenment.org/core/efl.git/commit/?id=f4198f022a4595efb4eb234b31af933510cb1575
commit f4198f022a4595efb4eb234b31af933510cb1575 Author: Gustavo Sverzut Barbieri <[email protected]> Date: Fri Oct 28 22:48:19 2016 -0200 efl_net_socket_ssl: initial SSL wrapper. This is the first step towards SSL connections on top of sockets, with an example on how to upgrade a dialer and a server client using TCP. --- src/Makefile_Ecore_Con.am | 21 +- src/examples/ecore/.gitignore | 2 + src/examples/ecore/Makefile.am | 15 +- .../ecore/efl_net_socket_ssl_dialer_example.c | 512 +++++++++++++++++ .../ecore/efl_net_socket_ssl_server_example.c | 351 ++++++++++++ src/lib/ecore_con/Ecore_Con_Eo.h | 6 + src/lib/ecore_con/ecore_con.c | 6 + src/lib/ecore_con/ecore_con_private.h | 4 + src/lib/ecore_con/efl_net_socket_ssl.c | 611 +++++++++++++++++++++ src/lib/ecore_con/efl_net_socket_ssl.eo | 90 +++ src/lib/ecore_con/efl_net_ssl_conn-gnutls.c | 362 ++++++++++++ src/lib/ecore_con/efl_net_ssl_conn-none.c | 50 ++ src/lib/ecore_con/efl_net_ssl_conn-openssl.c | 520 ++++++++++++++++++ src/lib/ecore_con/efl_net_ssl_context.c | 373 +++++++++++++ src/lib/ecore_con/efl_net_ssl_context.eo | 122 ++++ src/lib/ecore_con/efl_net_ssl_ctx-gnutls.c | 310 +++++++++++ src/lib/ecore_con/efl_net_ssl_ctx-none.c | 38 ++ src/lib/ecore_con/efl_net_ssl_ctx-openssl.c | 387 +++++++++++++ src/lib/ecore_con/efl_net_ssl_types.eot | 26 + 19 files changed, 3802 insertions(+), 4 deletions(-) diff --git a/src/Makefile_Ecore_Con.am b/src/Makefile_Ecore_Con.am index 49cf65b..53ea70d 100644 --- a/src/Makefile_Ecore_Con.am +++ b/src/Makefile_Ecore_Con.am @@ -20,6 +20,8 @@ ecore_con_eolian_files = \ lib/ecore_con/efl_net_server_tcp.eo \ lib/ecore_con/efl_net_server_udp.eo \ lib/ecore_con/efl_net_server_udp_client.eo \ + lib/ecore_con/efl_net_socket_ssl.eo \ + lib/ecore_con/efl_net_ssl_context.eo \ lib/ecore_con/ecore_con_eet_base.eo \ lib/ecore_con/ecore_con_eet_server_obj.eo \ lib/ecore_con/ecore_con_eet_client_obj.eo \ @@ -34,7 +36,8 @@ ecore_con_eolian_files += \ endif ecore_con_eolian_type_files = \ - lib/ecore_con/efl_net_http_types.eot + lib/ecore_con/efl_net_http_types.eot \ + lib/ecore_con/efl_net_ssl_types.eot ecore_con_eolian_c = $(ecore_con_eolian_files:%.eo=%.eo.c) @@ -95,10 +98,24 @@ lib/ecore_con/efl_net_server.c \ lib/ecore_con/efl_net_server_fd.c \ lib/ecore_con/efl_net_server_tcp.c \ lib/ecore_con/efl_net_server_udp.c \ -lib/ecore_con/efl_net_server_udp_client.c +lib/ecore_con/efl_net_server_udp_client.c \ +lib/ecore_con/efl_net_socket_ssl.c \ +lib/ecore_con/efl_net_ssl_context.c EXTRA_DIST2 += lib/ecore_con/ecore_con_legacy.c +# these are included rather than compiled out +# so the structures can be embedded into the +# object Private Data and allows functions to +# be all static +EXTRA_DIST2 += \ +lib/ecore_con/efl_net_ssl_conn-openssl.c \ +lib/ecore_con/efl_net_ssl_conn-gnutls.c \ +lib/ecore_con/efl_net_ssl_conn-none.c \ +lib/ecore_con/efl_net_ssl_ctx-openssl.c \ +lib/ecore_con/efl_net_ssl_ctx-gnutls.c \ +lib/ecore_con/efl_net_ssl_ctx-none.c + if HAVE_WINDOWS lib_ecore_con_libecore_con_la_SOURCES += lib/ecore_con/ecore_con_local_win32.c else diff --git a/src/examples/ecore/.gitignore b/src/examples/ecore/.gitignore index 0568bce..7068b7c 100644 --- a/src/examples/ecore/.gitignore +++ b/src/examples/ecore/.gitignore @@ -57,3 +57,5 @@ /efl_net_dialer_udp_example /efl_net_dialer_unix_example /ecore_evas_vnc +/efl_net_socket_ssl_dialer_example +/efl_net_socket_ssl_server_example diff --git a/src/examples/ecore/Makefile.am b/src/examples/ecore/Makefile.am index a0eb588..7e26163 100644 --- a/src/examples/ecore/Makefile.am +++ b/src/examples/ecore/Makefile.am @@ -85,7 +85,10 @@ efl_net_server_example \ efl_net_dialer_http_example \ efl_net_dialer_websocket_example \ efl_net_dialer_websocket_autobahntestee \ -efl_net_dialer_udp_example +efl_net_dialer_udp_example \ +efl_net_socket_ssl_dialer_example \ +efl_net_socket_ssl_server_example + ECORE_COMMON_LDADD = \ $(top_builddir)/src/lib/ecore/libecore.la \ @@ -320,6 +323,12 @@ efl_net_dialer_unix_example_SOURCES = efl_net_dialer_unix_example.c efl_net_dialer_unix_example_LDADD = $(ECORE_CON_COMMON_LDADD) endif +efl_net_socket_ssl_dialer_example_SOURCES = efl_net_socket_ssl_dialer_example.c +efl_net_socket_ssl_dialer_example_LDADD = $(ECORE_CON_COMMON_LDADD) + +efl_net_socket_ssl_server_example_SOURCES = efl_net_socket_ssl_server_example.c +efl_net_socket_ssl_server_example_LDADD = $(ECORE_CON_COMMON_LDADD) + SRCS = \ ecore_animator_example.c \ ecore_buffer_example.c \ @@ -374,7 +383,9 @@ efl_net_server_example.c \ efl_net_dialer_http_example.c \ efl_net_dialer_websocket_example.c \ efl_net_dialer_websocket_autobahntestee.c \ -efl_net_dialer_udp_example.c +efl_net_dialer_udp_example.c \ +efl_net_socket_ssl_dialer_example.c \ +efl_net_socket_ssl_server_example.c DATA_FILES = red.png Makefile.examples diff --git a/src/examples/ecore/efl_net_socket_ssl_dialer_example.c b/src/examples/ecore/efl_net_socket_ssl_dialer_example.c new file mode 100644 index 0000000..34abd16 --- /dev/null +++ b/src/examples/ecore/efl_net_socket_ssl_dialer_example.c @@ -0,0 +1,512 @@ +#define EFL_BETA_API_SUPPORT 1 +#define EFL_EO_API_SUPPORT 1 +#include <Ecore.h> +#include <Ecore_Con.h> +#include <Ecore_Getopt.h> +#include <fcntl.h> +#include <ctype.h> + +static int retval = EXIT_SUCCESS; +static Eina_List *pending_send = NULL; +static size_t pending_send_offset = 0; +static Eo *ssl_ctx; + +static void +_ssl_ready(void *data EINA_UNUSED, const Efl_Event *event EINA_UNUSED) +{ + fprintf(stderr, "INFO: SSL ready!\n"); +} + +static void +_ssl_error(void *data EINA_UNUSED, const Efl_Event *event) +{ + const Eina_Error *perr = event->info; + fprintf(stderr, "INFO: SSL error: %d '%s'\n", *perr, eina_error_msg_get(*perr)); + retval = EXIT_FAILURE; +} + +static void +_ssl_can_read(void *data EINA_UNUSED, const Efl_Event *event) +{ + char buf[63]; /* INFO: SSL read '...' will fit in 80 columns */ + Eina_Error err; + Eina_Bool can_read = efl_io_reader_can_read_get(event->object); + + /* NOTE: this message may appear with can read=0 BEFORE + * "read '...'" because efl_io_readr_read() will change the status + * of can_read to FALSE prior to return so we can print it! + */ + fprintf(stderr, "INFO: SSL can read=%d\n", can_read); + if (!can_read) return; + + do + { + Eina_Rw_Slice rw_slice; + + /* reset on every read, as rw_slice will be modified! */ + rw_slice.len = sizeof(buf); + rw_slice.mem = buf; + err = efl_io_reader_read(event->object, &rw_slice); + if (err) + { + if (err == EAGAIN) return; + fprintf(stderr, "ERROR: could not read: %s\n", eina_error_msg_get(err)); + retval = EXIT_FAILURE; + ecore_main_loop_quit(); + return; + } + + fprintf(stderr, "INFO: SSL read '" EINA_SLICE_STR_FMT "'\n", EINA_SLICE_STR_PRINT(rw_slice)); + } + while (efl_io_reader_can_read_get(event->object)); +} + +static void +_ssl_eos(void *data EINA_UNUSED, const Efl_Event *event EINA_UNUSED) +{ + fprintf(stderr, "INFO: SSL eos\n"); + ecore_main_loop_quit(); +} + +static void +_ssl_can_write(void *data EINA_UNUSED, const Efl_Event *event) +{ + Eina_Bool can_write = efl_io_writer_can_write_get(event->object); + size_t len; + + /* NOTE: this message may appear with can write=0 BEFORE + * "wrote '...'" because efl_io_writer_write() will change the status + * of can_write to FALSE prior to return so we can print it! + */ + fprintf(stderr, "INFO: SSL can write=%d (needed writes=%u offset=%zd)\n", can_write, eina_list_count(pending_send), pending_send_offset); + if (!can_write) return; + + if (!pending_send) return; + + do + { + Eina_Slice slice; + Eina_Error err; + + slice.bytes = pending_send->data; + slice.bytes += pending_send_offset; + slice.len = len = strlen(slice.mem); + + err = efl_io_writer_write(event->object, &slice, NULL); + if (err) + { + if (err == EAGAIN) return; + fprintf(stderr, "ERROR: could not write: %s\n", eina_error_msg_get(err)); + retval = EXIT_FAILURE; + ecore_main_loop_quit(); + return; + } + + fprintf(stderr, "INFO: SSL wrote '" EINA_SLICE_STR_FMT "'\n", EINA_SLICE_STR_PRINT(slice)); + + pending_send_offset += slice.len; + if (pending_send_offset == strlen(pending_send->data)) + { + free(pending_send->data); + pending_send = eina_list_remove_list(pending_send, pending_send); + pending_send_offset = 0; + } + } + while ((pending_send) && (efl_io_writer_can_write_get(event->object))); +} + +static void +_ssl_closed(void *data EINA_UNUSED, const Efl_Event *event) +{ + fprintf(stderr, "INFO: SSL closed\n"); + efl_del(event->object); +} + +EFL_CALLBACKS_ARRAY_DEFINE(ssl_cbs, + { EFL_NET_SOCKET_SSL_EVENT_SSL_READY, _ssl_ready }, + { EFL_NET_SOCKET_SSL_EVENT_SSL_ERROR, _ssl_error }, + { EFL_IO_READER_EVENT_CAN_READ_CHANGED, _ssl_can_read }, + { EFL_IO_READER_EVENT_EOS, _ssl_eos }, + { EFL_IO_WRITER_EVENT_CAN_WRITE_CHANGED, _ssl_can_write }, + { EFL_IO_CLOSER_EVENT_CLOSED, _ssl_closed }); + +static void +_connected(void *data EINA_UNUSED, const Efl_Event *event) +{ + Eo *ssl; + const char *hostname; + + fprintf(stderr, + "INFO: connected to '%s' (%s)\n" + "INFO: - local address=%s\n", + efl_net_dialer_address_dial_get(event->object), + efl_net_socket_address_remote_get(event->object), + efl_net_socket_address_local_get(event->object)); + + ssl = efl_add(EFL_NET_SOCKET_SSL_CLASS, efl_parent_get(event->object), + efl_net_socket_ssl_adopt(efl_added, event->object, ssl_ctx), + efl_event_callback_array_add(efl_added, ssl_cbs(), NULL)); + if (!ssl) + { + fprintf(stderr, "ERROR: failed to wrap dialer=%p in SSL\n", event->object); + retval = EXIT_FAILURE; + ecore_main_loop_quit(); + return; + } + + hostname = efl_net_socket_ssl_hostname_override_get(ssl); + if (!hostname) hostname = "<none>"; + + fprintf(stderr, + "INFO: - verify-mode=%d\n" + "INFO: - hostname-verify=%d\n" + "INFO: - hostname-override='%s'\n", + efl_net_socket_ssl_verify_mode_get(ssl), + efl_net_socket_ssl_hostname_verify_get(ssl), + hostname); +} + +static void +_resolved(void *data EINA_UNUSED, const Efl_Event *event) +{ + fprintf(stderr, "INFO: resolved %s => %s\n", + efl_net_dialer_address_dial_get(event->object), + efl_net_socket_address_remote_get(event->object)); +} + +static void +_error(void *data EINA_UNUSED, const Efl_Event *event) +{ + const Eina_Error *perr = event->info; + fprintf(stderr, "INFO: error: %d '%s'\n", *perr, eina_error_msg_get(*perr)); + retval = EXIT_FAILURE; +} + +EFL_CALLBACKS_ARRAY_DEFINE(dialer_cbs, + { EFL_NET_DIALER_EVENT_CONNECTED, _connected }, + { EFL_NET_DIALER_EVENT_RESOLVED, _resolved }, + { EFL_NET_DIALER_EVENT_ERROR, _error }); + + +static char * +_unescape(const char *str) +{ + char *ret = strdup(str); + char *c, *w; + Eina_Bool escaped = EINA_FALSE; + + for (c = ret, w = ret; *c != '\0'; c++) + { + if (escaped) + { + escaped = EINA_FALSE; + switch (*c) + { + case 'n': *w = '\n'; break; + case 'r': *w = '\r'; break; + case 't': *w = '\t'; break; + default: w++; /* no change */ + } + w++; + } + else + { + if (*c == '\\') + escaped = EINA_TRUE; + else + w++; + } + } + *w = '\0'; + return ret; +} + +/* + * Define USE_DEFAULT_CONTEXT will remove all context-setup functions + * and use a default context for dialers, what most applications + * should use. + */ +//#define USE_DEFAULT_CONTEXT 1 + +#ifndef USE_DEFAULT_CONTEXT +static const char *verify_mode_strs[] = { + "none", + "optional", + "required", + NULL +}; + +static const char *ciphers_strs[] = { + "auto", + "sslv3", + "tlsv1", + "tlsv1.1", + "tlsv1.2", + NULL +}; +#endif + +static const Ecore_Getopt options = { + "efl_net_socket_ssl_dialer_example", /* program name */ + NULL, /* usage line */ + "1", /* version */ + "(C) 2016 Enlightenment Project", /* copyright */ + "BSD 2-Clause", /* license */ + /* long description, may be multiline and contain \n */ + "Example of 'upgrading' a regular Efl.Net.Dialer.Tcp to a SSL socket.", + EINA_FALSE, + { + ECORE_GETOPT_STORE_STR('d', "line-delimiter", + "Changes the line delimiter to be used in both send and receive. Defaults to \\r\\n"), + ECORE_GETOPT_APPEND('s', "send", "send the given string to the server once connected.", ECORE_GETOPT_TYPE_STR), + +#ifndef USE_DEFAULT_CONTEXT + ECORE_GETOPT_CHOICE('c', "cipher", "Cipher to use, defaults to 'auto'", ciphers_strs), + ECORE_GETOPT_APPEND(0, "certificate", "certificate path to use.", ECORE_GETOPT_TYPE_STR), + ECORE_GETOPT_APPEND(0, "private-key", "private key path to use.", ECORE_GETOPT_TYPE_STR), + ECORE_GETOPT_APPEND(0, "crl", "certificate revogation list to use.", ECORE_GETOPT_TYPE_STR), + ECORE_GETOPT_APPEND(0, "ca", "certificate authorities path to use.", ECORE_GETOPT_TYPE_STR), + + ECORE_GETOPT_STORE_FALSE(0, "no-default-paths", "Do not use default certificate paths from your system."), + ECORE_GETOPT_CHOICE(0, "verify-mode", "One of: none (do not verify), optional (verify if provided), required (require and verify). Defaults to required", verify_mode_strs), + ECORE_GETOPT_STORE_FALSE(0, "no-hostname-verify", "Do not Verify hostname"), + ECORE_GETOPT_STORE_STR(0, "hostname-override", "Use this hostname instead of server provided one"), +#endif + + ECORE_GETOPT_VERSION('V', "version"), + ECORE_GETOPT_COPYRIGHT('C', "copyright"), + ECORE_GETOPT_LICENSE('L', "license"), + ECORE_GETOPT_HELP('h', "help"), + ECORE_GETOPT_STORE_METAVAR_STR(0, NULL, + "The address (URL) to dial", "address"), + ECORE_GETOPT_SENTINEL + } +}; + +int +main(int argc, char **argv) +{ + char *address = NULL; + char *line_delimiter_str = NULL; + char *str; + Eina_List *to_send = NULL; +#ifndef USE_DEFAULT_CONTEXT + Eina_Iterator *it; + char *verify_mode_choice = "required"; + char *cipher_choice = "auto"; + Eina_List *certificates = NULL; + Eina_List *private_keys = NULL; + Eina_List *crls = NULL; + Eina_List *cas = NULL; + Eina_Bool default_paths_load = EINA_TRUE; + Efl_Net_Ssl_Verify_Mode verify_mode = EFL_NET_SSL_VERIFY_MODE_OPTIONAL; + Efl_Net_Ssl_Cipher cipher = EFL_NET_SSL_CIPHER_AUTO; + Eina_Bool hostname_verify = EINA_TRUE; + char *hostname_override = NULL; +#endif + Eina_Bool quit_option = EINA_FALSE; + double timeout_dial = 30.0; + Ecore_Getopt_Value values[] = { + ECORE_GETOPT_VALUE_STR(line_delimiter_str), + ECORE_GETOPT_VALUE_LIST(to_send), + +#ifndef USE_DEFAULT_CONTEXT + ECORE_GETOPT_VALUE_STR(cipher_choice), + + ECORE_GETOPT_VALUE_LIST(certificates), + ECORE_GETOPT_VALUE_LIST(private_keys), + ECORE_GETOPT_VALUE_LIST(crls), + ECORE_GETOPT_VALUE_LIST(cas), + + ECORE_GETOPT_VALUE_BOOL(default_paths_load), + ECORE_GETOPT_VALUE_STR(verify_mode_choice), + ECORE_GETOPT_VALUE_BOOL(hostname_verify), + ECORE_GETOPT_VALUE_STR(hostname_override), +#endif + + /* standard block to provide version, copyright, license and help */ + ECORE_GETOPT_VALUE_BOOL(quit_option), /* -V/--version quits */ + ECORE_GETOPT_VALUE_BOOL(quit_option), /* -C/--copyright quits */ + ECORE_GETOPT_VALUE_BOOL(quit_option), /* -L/--license quits */ + ECORE_GETOPT_VALUE_BOOL(quit_option), /* -h/--help quits */ + + /* positional argument */ + ECORE_GETOPT_VALUE_STR(address), + + ECORE_GETOPT_VALUE_NONE /* sentinel */ + }; + int args; + Eo *dialer, *loop; + Eina_Error err; + + ecore_init(); + ecore_con_init(); + + args = ecore_getopt_parse(&options, values, argc, argv); + if (args < 0) + { + fputs("ERROR: Could not parse command line options.\n", stderr); + retval = EXIT_FAILURE; + goto end; + } + + if (quit_option) goto end; + + loop = ecore_main_loop_get(); + + args = ecore_getopt_parse_positional(&options, values, argc, argv, args); + if (args < 0) + { + fputs("ERROR: Could not parse positional arguments.\n", stderr); + retval = EXIT_FAILURE; + goto end; + } + +#ifndef USE_DEFAULT_CONTEXT + if (verify_mode_choice) + { + if (strcmp(verify_mode_choice, "none") == 0) + verify_mode = EFL_NET_SSL_VERIFY_MODE_NONE; + else if (strcmp(verify_mode_choice, "optional") == 0) + verify_mode = EFL_NET_SSL_VERIFY_MODE_OPTIONAL; + else if (strcmp(verify_mode_choice, "required") == 0) + verify_mode = EFL_NET_SSL_VERIFY_MODE_REQUIRED; + } + + if (cipher_choice) + { + if (strcmp(cipher_choice, "auto") == 0) + cipher = EFL_NET_SSL_CIPHER_AUTO; + else if (strcmp(cipher_choice, "sslv3") == 0) + cipher = EFL_NET_SSL_CIPHER_SSLV3; + else if (strcmp(cipher_choice, "tlsv1") == 0) + cipher = EFL_NET_SSL_CIPHER_TLSV1; + else if (strcmp(cipher_choice, "tlsv1.1") == 0) + cipher = EFL_NET_SSL_CIPHER_TLSV1_1; + else if (strcmp(cipher_choice, "tlsv1.2") == 0) + cipher = EFL_NET_SSL_CIPHER_TLSV1_2; + } +#endif + + if (to_send) + { + line_delimiter_str = _unescape(line_delimiter_str ? line_delimiter_str : "\\r\\n"); + if (line_delimiter_str[0] == '\0') + { + pending_send = to_send; + to_send = NULL; + } + else + { + EINA_LIST_FREE(to_send, str) + { + /* ignore empty sends, but add line delimiter, so we can do HTTP's last line :-) */ + if (str[0] == '\0') + free(str); + else + pending_send = eina_list_append(pending_send, str); + + if (line_delimiter_str[0]) + pending_send = eina_list_append(pending_send, strdup(line_delimiter_str)); + } + } + free(line_delimiter_str); + line_delimiter_str = NULL; + } + + /* create a new SSL context with command line configurations. + * another option would be to use the default dialer context */ +#ifndef USE_DEFAULT_CONTEXT + ssl_ctx = efl_add(EFL_NET_SSL_CONTEXT_CLASS, NULL, + efl_net_ssl_context_certificates_set(efl_added, eina_list_iterator_new(certificates)), + efl_net_ssl_context_private_keys_set(efl_added, eina_list_iterator_new(private_keys)), + efl_net_ssl_context_certificate_revogation_lists_set(efl_added, eina_list_iterator_new(crls)), + efl_net_ssl_context_certificate_authorities_set(efl_added, eina_list_iterator_new(cas)), + efl_net_ssl_context_default_paths_load_set(efl_added, default_paths_load), + efl_net_ssl_context_verify_mode_set(efl_added, verify_mode), + efl_net_ssl_context_hostname_verify_set(efl_added, hostname_verify), + efl_net_ssl_context_hostname_set(efl_added, hostname_override), + efl_net_ssl_context_setup(efl_added, cipher, EINA_TRUE)); +#else + ssl_ctx = efl_net_ssl_context_default_dialer_get(EFL_NET_SSL_CONTEXT_CLASS); + fprintf(stderr, "INFO: using default context for dialers.\n"); +#endif + + if (!ssl_ctx) + { + fprintf(stderr, "ERROR: could not create the SSL context!\n"); + retval = EXIT_FAILURE; + goto no_ssl_ctx; + } + + /* no point in printing default context, it's all empty */ +#ifndef USE_DEFAULT_CONTEXT + fprintf(stderr, "INFO: - certificates in use:\n"); + it = efl_net_ssl_context_certificates_get(ssl_ctx); + EINA_ITERATOR_FOREACH(it, str) + fprintf(stderr, "INFO: * %s\n", str); + eina_iterator_free(it); + + fprintf(stderr, "INFO: - private keys in use:\n"); + it = efl_net_ssl_context_private_keys_get(ssl_ctx); + EINA_ITERATOR_FOREACH(it, str) + fprintf(stderr, "INFO: * %s\n", str); + eina_iterator_free(it); + + fprintf(stderr, "INFO: - certificate revogation lists in use:\n"); + it = efl_net_ssl_context_certificate_revogation_lists_get(ssl_ctx); + EINA_ITERATOR_FOREACH(it, str) + fprintf(stderr, "INFO: * %s\n", str); + eina_iterator_free(it); + + fprintf(stderr, "INFO: - certificate authorities in use:\n"); + it = efl_net_ssl_context_certificate_authorities_get(ssl_ctx); + EINA_ITERATOR_FOREACH(it, str) + fprintf(stderr, "INFO: * %s\n", str); + eina_iterator_free(it); + +#endif + + fprintf(stderr, + "INFO: - verify-mode=%d\n", + efl_net_ssl_context_verify_mode_get(ssl_ctx)); + + dialer = efl_add(EFL_NET_DIALER_TCP_CLASS, loop, + efl_name_set(efl_added, "dialer"), + efl_net_dialer_timeout_dial_set(efl_added, timeout_dial), + efl_event_callback_array_add(efl_added, dialer_cbs(), NULL)); + + err = efl_net_dialer_dial(dialer, address); + if (err != 0) + { + fprintf(stderr, "ERROR: could not dial '%s': %s", + address, eina_error_msg_get(err)); + retval = EXIT_FAILURE; + goto no_mainloop; + } + + ecore_main_loop_begin(); + + fprintf(stderr, "INFO: main loop finished.\n"); + + no_mainloop: + efl_io_closer_close(dialer); /* just del won't do as ssl has an extra ref */ + efl_del(dialer); + no_ssl_ctx: + efl_del(ssl_ctx); + + end: + EINA_LIST_FREE(pending_send, str) free(str); + +#ifndef USE_DEFAULT_CONTEXT + EINA_LIST_FREE(certificates, str) free(str); + EINA_LIST_FREE(private_keys, str) free(str); + EINA_LIST_FREE(crls, str) free(str); + EINA_LIST_FREE(cas, str) free(str); +#endif + + ecore_con_shutdown(); + ecore_shutdown(); + + return retval; +} diff --git a/src/examples/ecore/efl_net_socket_ssl_server_example.c b/src/examples/ecore/efl_net_socket_ssl_server_example.c new file mode 100644 index 0000000..c87fec3 --- /dev/null +++ b/src/examples/ecore/efl_net_socket_ssl_server_example.c @@ -0,0 +1,351 @@ +#define EFL_BETA_API_SUPPORT 1 +#define EFL_EO_API_SUPPORT 1 +#include <Ecore.h> +#include <Ecore_Con.h> +#include <Ecore_Getopt.h> +#include <fcntl.h> + +static int retval = EXIT_SUCCESS; +static double timeout = 30.0; +static Eo *ssl_ctx = NULL; + +/* NOTE: client i/o events are only used as debug, you can omit these */ + +static void +_ssl_can_read_changed(void *data EINA_UNUSED, const Efl_Event *event) +{ + fprintf(stderr, "INFO: ssl %s can_read=%d\n", + efl_net_socket_address_remote_get(event->object), + efl_io_reader_can_read_get(event->object)); +} + +static void +_ssl_can_write_changed(void *data EINA_UNUSED, const Efl_Event *event) +{ + fprintf(stderr, "INFO: ssl %s can_write=%d\n", + efl_net_socket_address_remote_get(event->object), + efl_io_writer_can_write_get(event->object)); +} + +static void +_ssl_eos(void *data EINA_UNUSED, const Efl_Event *event) +{ + fprintf(stderr, "INFO: ssl %s eos.\n", + efl_net_socket_address_remote_get(event->object)); +} + +static void +_ssl_closed(void *data EINA_UNUSED, const Efl_Event *event) +{ + fprintf(stderr, "INFO: ssl %s closed.\n", + efl_net_socket_address_remote_get(event->object)); +} + +EFL_CALLBACKS_ARRAY_DEFINE(ssl_cbs, + { EFL_IO_READER_EVENT_CAN_READ_CHANGED, _ssl_can_read_changed }, + { EFL_IO_READER_EVENT_EOS, _ssl_eos }, + { EFL_IO_WRITER_EVENT_CAN_WRITE_CHANGED, _ssl_can_write_changed }, + { EFL_IO_CLOSER_EVENT_CLOSED, _ssl_closed }); + + +/* copier events are of interest, you should hook to at least "done" + * and "error" + */ + +/* echo copier is about the same socket, you can close it right away */ + +static void +_echo_copier_done(void *data EINA_UNUSED, const Efl_Event *event) +{ + Eo *copier = event->object; + fprintf(stderr, "INFO: echo copier done, close and del %p\n", copier); + efl_del(copier); /* set to close_on_destructor, will auto close copier and ssl */ +} + +static void +_echo_copier_error(void *data EINA_UNUSED, const Efl_Event *event) +{ + Eo *copier = event->object; + const Eina_Error *perr = event->info; + + if (*perr == ETIMEDOUT) + { + Eo *ssl = efl_io_copier_source_get(copier); + fprintf(stderr, "INFO: ssl '%s' timed out, delete it.\n", + efl_net_socket_address_remote_get(ssl)); + efl_del(copier); + return; + } + + retval = EXIT_FAILURE; + + fprintf(stderr, "ERROR: echo copier %p failed %d '%s', close and del.\n", + copier, *perr, eina_error_msg_get(*perr)); + + efl_del(copier); +} + +EFL_CALLBACKS_ARRAY_DEFINE(echo_copier_cbs, + { EFL_IO_COPIER_EVENT_DONE, _echo_copier_done }, + { EFL_IO_COPIER_EVENT_ERROR, _echo_copier_error}); + +/* server events are mandatory, afterall you need to define what's + * going to happen after a client socket is connected. This is the + * "client,add" event. + */ +static void +_server_client_add(void *data EINA_UNUSED, const Efl_Event *event) +{ + Efl_Net_Socket *client = event->info; + Efl_Net_Socket_Ssl *ssl; + Eo *echo_copier; + + fprintf(stderr, "INFO: accepted client %s\n", + efl_net_socket_address_remote_get(client)); + + /* to use a client, you must efl_ref() it. Here we're not doing it + * explicitly because Efl.Net.Socket.Ssl do take a reference. + */ + ssl = efl_add(EFL_NET_SOCKET_SSL_CLASS, efl_loop_get(client), + efl_net_socket_ssl_adopt(efl_added, client, ssl_ctx), /* mandatory inside efl_add() */ + efl_event_callback_array_add(efl_added, ssl_cbs(), NULL) /* optional, for debug purposes */ + ); + if (!ssl) + { + fprintf(stderr, "ERROR: failed to wrap client=%p in SSL\n", client); + retval = EXIT_FAILURE; + ecore_main_loop_quit(); + return; + } + + /* + * SSL will do a handshake and keep can_read/can_write as false + * until it's finished, thus we can create the echo copier right + * away. + * + * remember to NEVER add a copier, read or write from wrapped + * socket, doing that will bypass the SSL layer and thus result in + * incorrect operation. You can forget about it + */ + + echo_copier = efl_add(EFL_IO_COPIER_CLASS, efl_parent_get(ssl), + efl_io_copier_source_set(efl_added, ssl), + efl_io_copier_destination_set(efl_added, ssl), + efl_io_copier_inactivity_timeout_set(efl_added, timeout), + efl_event_callback_array_add(efl_added, echo_copier_cbs(), ssl), + efl_io_closer_close_on_destructor_set(efl_added, EINA_TRUE) /* we want to auto-close as we have a single copier */ + ); + + fprintf(stderr, "INFO: using an echo copier=%p for ssl %s\n", + echo_copier, efl_net_socket_address_remote_get(ssl)); +} + +static void +_server_error(void *data EINA_UNUSED, const Efl_Event *event) +{ + const Eina_Error *perr = event->info; + fprintf(stderr, "ERROR: %d '%s'\n", *perr, eina_error_msg_get(*perr)); + retval = EXIT_FAILURE; + ecore_main_loop_quit(); +} + +static void +_server_serving(void *data EINA_UNUSED, const Efl_Event *event) +{ + fprintf(stderr, "INFO: serving at %s\n", + efl_net_server_address_get(event->object)); +} + +EFL_CALLBACKS_ARRAY_DEFINE(server_cbs, + { EFL_NET_SERVER_EVENT_CLIENT_ADD, _server_client_add }, + { EFL_NET_SERVER_EVENT_ERROR, _server_error }, + { EFL_NET_SERVER_EVENT_SERVING, _server_serving }); + +static const char *ciphers_strs[] = { + "auto", + "sslv3", + "tlsv1", + "tlsv1.1", + "tlsv1.2", + NULL +}; + +static const Ecore_Getopt options = { + "efl_net_socket_ssl_server_example", /* program name */ + NULL, /* usage line */ + "1", /* version */ + "(C) 2016 Enlightenment Project", /* copyright */ + "BSD 2-Clause", /* license */ + /* long description, may be multiline and contain \n */ + "Example of 'upgrading' a regular Efl.Net.Socket received from an Efl.Net.Server.Tcp to a SSL socket, then serving as 'echo' server.", + EINA_FALSE, + { + ECORE_GETOPT_CHOICE('c', "cipher", "Cipher to use, defaults to 'auto'", ciphers_strs), + + ECORE_GETOPT_APPEND(0, "certificate", "certificate path to use.", ECORE_GETOPT_TYPE_STR), + ECORE_GETOPT_APPEND(0, "private-key", "private key path to use.", ECORE_GETOPT_TYPE_STR), + ECORE_GETOPT_APPEND(0, "crl", "certificate revogation list to use.", ECORE_GETOPT_TYPE_STR), + ECORE_GETOPT_APPEND(0, "ca", "certificate authorities path to use.", ECORE_GETOPT_TYPE_STR), + + ECORE_GETOPT_VERSION('V', "version"), + ECORE_GETOPT_COPYRIGHT('C', "copyright"), + ECORE_GETOPT_LICENSE('L', "license"), + ECORE_GETOPT_HELP('h', "help"), + + ECORE_GETOPT_STORE_METAVAR_STR(0, NULL, + "The server address to listen, such as " + "IPv4:PORT, [IPv6]:PORT, Unix socket path...", + "address"), + + ECORE_GETOPT_SENTINEL + } +}; + +int +main(int argc, char **argv) +{ + char *address = NULL; + char *cipher_choice = "auto"; + char *str; + Eina_List *certificates = NULL; + Eina_List *private_keys = NULL; + Eina_List *crls = NULL; + Eina_List *cas = NULL; + Efl_Net_Ssl_Cipher cipher = EFL_NET_SSL_CIPHER_AUTO; + Eina_Bool quit_option = EINA_FALSE; + Ecore_Getopt_Value values[] = { + ECORE_GETOPT_VALUE_STR(cipher_choice), + + ECORE_GETOPT_VALUE_LIST(certificates), + ECORE_GETOPT_VALUE_LIST(private_keys), + ECORE_GETOPT_VALUE_LIST(crls), + ECORE_GETOPT_VALUE_LIST(cas), + + /* standard block to provide version, copyright, license and help */ + ECORE_GETOPT_VALUE_BOOL(quit_option), /* -V/--version quits */ + ECORE_GETOPT_VALUE_BOOL(quit_option), /* -C/--copyright quits */ + ECORE_GETOPT_VALUE_BOOL(quit_option), /* -L/--license quits */ + ECORE_GETOPT_VALUE_BOOL(quit_option), /* -h/--help quits */ + + /* positional argument */ + ECORE_GETOPT_VALUE_STR(address), + + ECORE_GETOPT_VALUE_NONE /* sentinel */ + }; + int args; + Eina_Iterator *it; + Eo *server; + Eina_Error err; + + ecore_init(); + ecore_con_init(); + + args = ecore_getopt_parse(&options, values, argc, argv); + if (args < 0) + { + fputs("ERROR: Could not parse command line options.\n", stderr); + retval = EXIT_FAILURE; + goto end; + } + + if (quit_option) goto end; + + args = ecore_getopt_parse_positional(&options, values, argc, argv, args); + if (args < 0) + { + fputs("ERROR: Could not parse positional arguments.\n", stderr); + retval = EXIT_FAILURE; + goto end; + } + + if (cipher_choice) + { + if (strcmp(cipher_choice, "auto") == 0) + cipher = EFL_NET_SSL_CIPHER_AUTO; + else if (strcmp(cipher_choice, "sslv3") == 0) + cipher = EFL_NET_SSL_CIPHER_SSLV3; + else if (strcmp(cipher_choice, "tlsv1") == 0) + cipher = EFL_NET_SSL_CIPHER_TLSV1; + else if (strcmp(cipher_choice, "tlsv1.1") == 0) + cipher = EFL_NET_SSL_CIPHER_TLSV1_1; + else if (strcmp(cipher_choice, "tlsv1.2") == 0) + cipher = EFL_NET_SSL_CIPHER_TLSV1_2; + } + + ssl_ctx = efl_add(EFL_NET_SSL_CONTEXT_CLASS, NULL, + efl_net_ssl_context_certificates_set(efl_added, eina_list_iterator_new(certificates)), + efl_net_ssl_context_private_keys_set(efl_added, eina_list_iterator_new(private_keys)), + efl_net_ssl_context_certificate_revogation_lists_set(efl_added, eina_list_iterator_new(crls)), + efl_net_ssl_context_certificate_authorities_set(efl_added, eina_list_iterator_new(cas)), + efl_net_ssl_context_setup(efl_added, cipher, EINA_FALSE /* a server! */)); + + if (!ssl_ctx) + { + fprintf(stderr, "ERROR: could not create the SSL context!\n"); + retval = EXIT_FAILURE; + goto end; + } + + fprintf(stderr, "INFO: - certificates in use:\n"); + it = efl_net_ssl_context_certificates_get(ssl_ctx); + EINA_ITERATOR_FOREACH(it, str) + fprintf(stderr, "INFO: * %s\n", str); + eina_iterator_free(it); + + fprintf(stderr, "INFO: - private keys in use:\n"); + it = efl_net_ssl_context_private_keys_get(ssl_ctx); + EINA_ITERATOR_FOREACH(it, str) + fprintf(stderr, "INFO: * %s\n", str); + eina_iterator_free(it); + + fprintf(stderr, "INFO: - certificate revogation lists in use:\n"); + it = efl_net_ssl_context_certificate_revogation_lists_get(ssl_ctx); + EINA_ITERATOR_FOREACH(it, str) + fprintf(stderr, "INFO: * %s\n", str); + eina_iterator_free(it); + + fprintf(stderr, "INFO: - certificate authorities in use:\n"); + it = efl_net_ssl_context_certificate_authorities_get(ssl_ctx); + EINA_ITERATOR_FOREACH(it, str) + fprintf(stderr, "INFO: * %s\n", str); + eina_iterator_free(it); + + server = efl_add(EFL_NET_SERVER_TCP_CLASS, ecore_main_loop_get(), /* it's mandatory to use a main loop provider as the server parent */ + efl_net_server_tcp_ipv6_only_set(efl_added, EINA_FALSE), /* optional, but helps testing IPv4 on IPv6 servers */ + efl_net_server_fd_close_on_exec_set(efl_added, EINA_TRUE), /* recommended */ + efl_net_server_fd_reuse_address_set(efl_added, EINA_TRUE), /* optional, but nice for testing */ + efl_net_server_fd_reuse_port_set(efl_added, EINA_TRUE), /* optional, but nice for testing... not secure unless you know what you're doing */ + efl_event_callback_array_add(efl_added, server_cbs(), NULL)); /* mandatory to have "client,add" in order to be useful */ + if (!server) + { + fprintf(stderr, "ERROR: could not create class Efl.Net.Server.Tcp\n"); + goto end_ctx; + } + + err = efl_net_server_serve(server, address); + if (err) + { + fprintf(stderr, "ERROR: could not serve(%s): %s\n", + address, eina_error_msg_get(err)); + goto end_server; + } + + ecore_main_loop_begin(); + + end_server: + efl_del(server); + server = NULL; + end_ctx: + efl_del(ssl_ctx); + + end: + EINA_LIST_FREE(certificates, str) free(str); + EINA_LIST_FREE(private_keys, str) free(str); + EINA_LIST_FREE(crls, str) free(str); + EINA_LIST_FREE(cas, str) free(str); + + ecore_con_shutdown(); + ecore_shutdown(); + + return retval; +} diff --git a/src/lib/ecore_con/Ecore_Con_Eo.h b/src/lib/ecore_con/Ecore_Con_Eo.h index b3e3bf4..8e4cf65 100644 --- a/src/lib/ecore_con/Ecore_Con_Eo.h +++ b/src/lib/ecore_con/Ecore_Con_Eo.h @@ -31,3 +31,9 @@ #include "efl_net_dialer_http.eo.h" #include "efl_net_dialer_websocket.eo.h" + + +#include "efl_net_ssl_types.eot.h" + +#include "efl_net_ssl_context.eo.h" +#include "efl_net_socket_ssl.eo.h" diff --git a/src/lib/ecore_con/ecore_con.c b/src/lib/ecore_con/ecore_con.c index 6f401a0..be0c1a8 100644 --- a/src/lib/ecore_con/ecore_con.c +++ b/src/lib/ecore_con/ecore_con.c @@ -184,6 +184,9 @@ EWAPI Eina_Error EFL_NET_DIALER_ERROR_PROXY_AUTHENTICATION_FAILED = 0; EWAPI Eina_Error EFL_NET_SERVER_ERROR_COULDNT_RESOLVE_HOST = 0; +EWAPI Eina_Error EFL_NET_SOCKET_SSL_ERROR_HANDSHAKE = 0; +EWAPI Eina_Error EFL_NET_SOCKET_SSL_ERROR_CERTIFICATE_VERIFY_FAILED = 0; + static Eina_List *servers = NULL; static int _ecore_con_init_count = 0; static int _ecore_con_event_count = 0; @@ -241,6 +244,9 @@ ecore_con_init(void) EFL_NET_SERVER_ERROR_COULDNT_RESOLVE_HOST = eina_error_msg_static_register("Couldn't resolve host name"); + EFL_NET_SOCKET_SSL_ERROR_HANDSHAKE = eina_error_msg_static_register("Failed SSL handshake"); + EFL_NET_SOCKET_SSL_ERROR_CERTIFICATE_VERIFY_FAILED = eina_error_msg_static_register("Failed to verify peer's certificate"); + eina_magic_string_set(ECORE_MAGIC_CON_SERVER, "Ecore_Con_Server"); eina_magic_string_set(ECORE_MAGIC_CON_CLIENT, "Ecore_Con_Client"); eina_magic_string_set(ECORE_MAGIC_CON_URL, "Ecore_Con_Url"); diff --git a/src/lib/ecore_con/ecore_con_private.h b/src/lib/ecore_con/ecore_con_private.h index 2063932..ec2fc21 100644 --- a/src/lib/ecore_con/ecore_con_private.h +++ b/src/lib/ecore_con/ecore_con_private.h @@ -674,4 +674,8 @@ Eina_Error efl_net_multicast_loopback_get(SOCKET fd, int family, Eina_Bool *loop */ size_t efl_net_udp_datagram_size_query(SOCKET fd); + +/* SSL abstraction API */ +extern void *efl_net_ssl_context_connection_new(Efl_Net_Ssl_Context *context); + #endif diff --git a/src/lib/ecore_con/efl_net_socket_ssl.c b/src/lib/ecore_con/efl_net_socket_ssl.c new file mode 100644 index 0000000..eb27888 --- /dev/null +++ b/src/lib/ecore_con/efl_net_socket_ssl.c @@ -0,0 +1,611 @@ +#define EFL_NET_SOCKET_SSL_PROTECTED 1 +#define EFL_IO_READER_PROTECTED 1 +#define EFL_IO_WRITER_PROTECTED 1 +#define EFL_IO_CLOSER_PROTECTED 1 +#define EFL_NET_SOCKET_PROTECTED 1 + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif + +#include "Ecore.h" +#include "Ecore_Con.h" +#include "ecore_con_private.h" + +typedef struct _Efl_Net_Ssl_Conn Efl_Net_Ssl_Conn; + +/** + * Setups the SSL context + * + * Update the given lists, removing invalid entries. If all entries + * failed in a list, return EINVAL. + * + * @internal + */ +static Eina_Error efl_net_ssl_conn_setup(Efl_Net_Ssl_Conn *conn, Eina_Bool is_dialer, Efl_Net_Socket *sock, Efl_Net_Ssl_Context *context); + +/** + * Cleans up the SSL associated to this context. + * @internal + */ +static void efl_net_ssl_conn_teardown(Efl_Net_Ssl_Conn *conn); + +/** + * Send data to remote peer. + * + * This should be called once handshake is finished, otherwise it may + * lead the handshake to fail. + * + * @param slice[inout] takes the amount of bytes to write and + * source memory, will store written length in slice->len. + * + * @internal + */ +static Eina_Error efl_net_ssl_conn_write(Efl_Net_Ssl_Conn *conn, Eina_Slice *slice); + +/** + * Receive data from remote peer. + * + * This should be called once handshake is finished, otherwise it may + * lead the handshake to fail. + * + * Note that even if the socket 'can_read', eventually it couldn't + * decipher a byte and it will return slice->len == 0 with EAGAIN as + * error. + * + * @param slice[inout] takes the amount of bytes to read and + * destination memory, will store read length in slice->len. + * + * @internal + */ +static Eina_Error efl_net_ssl_conn_read(Efl_Net_Ssl_Conn *conn, Eina_Rw_Slice *slice); + +/** + * Attempt to finish the handshake. + * + * This should not block, if it's not finished yet, just set done = + * false. + * + * Errors, such as failed handshake, should be returned as Eina_Error. + * + * @internal + */ +static Eina_Error efl_net_ssl_conn_handshake(Efl_Net_Ssl_Conn *conn, Eina_Bool *done); + +/** + * Configure how to verify peer. + * + * @internal + */ +static Eina_Error efl_net_ssl_conn_verify_mode_set(Efl_Net_Ssl_Conn *conn, Efl_Net_Ssl_Verify_Mode verify_mode); + +/** + * Configure whenever to check for hostname. + * + * @internal + */ +static Eina_Error efl_net_ssl_conn_hostname_verify_set(Efl_Net_Ssl_Conn *conn, Eina_Bool hostname_verify); + +/** + * Overrides the hostname to use. + * + * @note duplicate hostname if needed! + * + * @internal + */ +static Eina_Error efl_net_ssl_conn_hostname_override_set(Efl_Net_Ssl_Conn *conn, const char *hostname); + +#if HAVE_OPENSSL +#include "efl_net_ssl_conn-openssl.c" +#elif HAVE_GNUTLS +#include "efl_net_ssl_conn-gnutls.c" +#else +#include "efl_net_ssl_conn-none.c" +#endif + +#define MY_CLASS EFL_NET_SOCKET_SSL_CLASS + +typedef struct _Efl_Net_Socket_Ssl_Data +{ + Eo *sock; + Efl_Net_Ssl_Context *context; + const char *hostname_override; + Efl_Net_Ssl_Conn ssl_conn; + Efl_Net_Ssl_Verify_Mode verify_mode; + Eina_Bool hostname_verify; + Eina_Bool did_handshake; + Eina_Bool can_read; + Eina_Bool eos; + Eina_Bool can_write; +} Efl_Net_Socket_Ssl_Data; + +static void +efl_net_socket_ssl_sock_eos(void *data, const Efl_Event *event EINA_UNUSED) +{ + Eo *o = data; + efl_io_reader_eos_set(o, EINA_TRUE); +} + +static void +efl_net_socket_ssl_handshake_try(Eo *o, Efl_Net_Socket_Ssl_Data *pd) +{ + Eina_Error err; + + DBG("SSL=%p handshake...", o); + + err = efl_net_ssl_conn_handshake(&pd->ssl_conn, &pd->did_handshake); + if (err) + { + WRN("SSL=%p failed handshake: %s", o, eina_error_msg_get(err)); + efl_event_callback_call(o, EFL_NET_SOCKET_SSL_EVENT_SSL_ERROR, &err); + efl_io_closer_close(o); + return; + } + if (!pd->did_handshake) return; + + DBG("SSL=%p finished handshake", o); + efl_io_reader_can_read_set(o, efl_io_reader_can_read_get(pd->sock)); + efl_io_writer_can_write_set(o, efl_io_writer_can_write_get(pd->sock)); + + efl_event_callback_call(o, EFL_NET_SOCKET_SSL_EVENT_SSL_READY, NULL); +} + +static void +efl_net_socket_ssl_sock_can_read_changed(void *data, const Efl_Event *event EINA_UNUSED) +{ + Eo *o = data; + Efl_Net_Socket_Ssl_Data *pd = efl_data_scope_get(o, MY_CLASS); + + efl_ref(o); /* we're emitting callbacks then continuing the workflow */ + + if (!efl_io_reader_can_read_get(pd->sock)) + { + // TODO: stop jobs? + goto end; + } + + if (pd->did_handshake) + efl_io_reader_can_read_set(o, EINA_TRUE); + else + efl_net_socket_ssl_handshake_try(o, pd); + + end: + efl_unref(o); +} + +static void +efl_net_socket_ssl_sock_can_write_changed(void *data, const Efl_Event *event EINA_UNUSED) +{ + Eo *o = data; + Efl_Net_Socket_Ssl_Data *pd = efl_data_scope_get(o, MY_CLASS); + + efl_ref(o); /* we're emitting callbacks then continuing the workflow */ + + if (!efl_io_writer_can_write_get(pd->sock)) + { + // TODO: stop jobs? + goto end; + } + + if (pd->did_handshake) + efl_io_writer_can_write_set(o, EINA_TRUE); + else + efl_net_socket_ssl_handshake_try(o, pd); + + end: + efl_unref(o); +} + +static void +efl_net_socket_ssl_sock_closed(void *data, const Efl_Event *event EINA_UNUSED) +{ + Eo *o = data; + efl_event_callback_call(o, EFL_IO_CLOSER_EVENT_CLOSED, NULL); +} + +static void +efl_net_socket_ssl_sock_del(void *data, const Efl_Event *event EINA_UNUSED) +{ + Eo *o = data; + Efl_Net_Socket_Ssl_Data *pd = efl_data_scope_get(o, MY_CLASS); + pd->sock = NULL; + efl_net_ssl_conn_teardown(&pd->ssl_conn); +} + +EFL_CALLBACKS_ARRAY_DEFINE(efl_net_socket_ssl_sock_cbs, + {EFL_IO_READER_EVENT_EOS, efl_net_socket_ssl_sock_eos}, + {EFL_IO_READER_EVENT_CAN_READ_CHANGED, efl_net_socket_ssl_sock_can_read_changed}, + {EFL_IO_WRITER_EVENT_CAN_WRITE_CHANGED, efl_net_socket_ssl_sock_can_write_changed}, + {EFL_IO_CLOSER_EVENT_CLOSED, efl_net_socket_ssl_sock_closed}, + {EFL_EVENT_DEL, efl_net_socket_ssl_sock_del}); + +static void +efl_net_socket_ssl_sock_connected(void *data, const Efl_Event *event EINA_UNUSED) +{ + Eo *o = data; + Efl_Net_Socket_Ssl_Data *pd = efl_data_scope_get(o, MY_CLASS); + Eina_Error err; + + + efl_ref(o); /* we're emitting callbacks then continuing the workflow */ + + err = efl_net_ssl_conn_handshake(&pd->ssl_conn, &pd->did_handshake); + if (err) + { + WRN("SSL=%p failed handshake: %s", o, eina_error_msg_get(err)); + efl_io_closer_close(o); + return; + } + + efl_unref(o); +} + +static void +_efl_net_socket_ssl_context_del(void *data, const Efl_Event *event EINA_UNUSED) +{ + Eo *o = data; + Efl_Net_Socket_Ssl_Data *pd = efl_data_scope_get(o, MY_CLASS); + pd->context = NULL; +} + +EOLIAN static void +_efl_net_socket_ssl_adopt(Eo *o, Efl_Net_Socket_Ssl_Data *pd, Efl_Net_Socket *sock, Efl_Net_Ssl_Context *context) +{ + Eina_Error err; + char *tmp = NULL; + const char *hostname; + Eina_Bool is_dialer; + + EINA_SAFETY_ON_TRUE_RETURN(pd->sock != NULL); + EINA_SAFETY_ON_FALSE_RETURN(efl_isa(sock, EFL_NET_SOCKET_INTERFACE)); + EINA_SAFETY_ON_FALSE_RETURN(efl_isa(context, EFL_NET_SSL_CONTEXT_CLASS)); + EINA_SAFETY_ON_TRUE_RETURN(efl_finalized_get(o)); + + is_dialer = efl_isa(sock, EFL_NET_DIALER_INTERFACE); + err = efl_net_ssl_conn_setup(&pd->ssl_conn, is_dialer, sock, context); + if (err) + { + ERR("ssl=%p failed to adopt socket (is_dialer=%d) sock=%p", o, is_dialer, sock); + return; + } + + pd->context = efl_ref(context); + efl_event_callback_add(context, EFL_EVENT_DEL, _efl_net_socket_ssl_context_del, o); + + DBG("ssl=%p adopted socket (is_dialer=%d) sock=%p, ssl_conn=%p", o, is_dialer, sock, &pd->ssl_conn); + + if (pd->hostname_verify == 0xff) + pd->hostname_verify = efl_net_ssl_context_hostname_verify_get(context); + + hostname = pd->hostname_override; + if (!hostname) + hostname = pd->hostname_override = eina_stringshare_ref(efl_net_ssl_context_hostname_get(context)); + if (!hostname) + { + const char *remote_address = (is_dialer ? + efl_net_dialer_address_dial_get(sock) : + efl_net_socket_address_remote_get(sock)); + if (remote_address) + { + const char *host, *port; + + tmp = strdup(remote_address); + EINA_SAFETY_ON_NULL_RETURN(tmp); + if (efl_net_ip_port_split(tmp, &host, &port)) + hostname = host; + } + } + + if ((uint8_t)pd->verify_mode == 0xff) pd->verify_mode = efl_net_ssl_context_verify_mode_get(context); + efl_net_ssl_conn_verify_mode_set(&pd->ssl_conn, pd->verify_mode); + efl_net_ssl_conn_hostname_verify_set(&pd->ssl_conn, pd->hostname_verify); + efl_net_ssl_conn_hostname_override_set(&pd->ssl_conn, hostname); + free(tmp); + + pd->sock = efl_ref(sock); + efl_event_callback_array_add(sock, efl_net_socket_ssl_sock_cbs(), o); + + if (efl_isa(sock, EFL_NET_DIALER_INTERFACE)) + efl_event_callback_add(sock, EFL_NET_DIALER_EVENT_CONNECTED, efl_net_socket_ssl_sock_connected, o); + + efl_net_socket_ssl_sock_can_read_changed(o, NULL); + efl_net_socket_ssl_sock_can_write_changed(o, NULL); + if (efl_io_closer_closed_get(sock)) + efl_event_callback_call(o, EFL_IO_CLOSER_EVENT_CLOSED, NULL); +} + +static Efl_Net_Ssl_Verify_Mode +_efl_net_socket_ssl_verify_mode_get(Eo *o EINA_UNUSED, Efl_Net_Socket_Ssl_Data *pd) +{ + return pd->verify_mode; +} + +static void +_efl_net_socket_ssl_verify_mode_set(Eo *o EINA_UNUSED, Efl_Net_Socket_Ssl_Data *pd, Efl_Net_Ssl_Verify_Mode verify_mode) +{ + pd->verify_mode = verify_mode; + if (!efl_finalized_get(o)) return; + + efl_net_ssl_conn_verify_mode_set(&pd->ssl_conn, pd->verify_mode); +} + +static Eina_Bool +_efl_net_socket_ssl_hostname_verify_get(Eo *o EINA_UNUSED, Efl_Net_Socket_Ssl_Data *pd) +{ + return pd->hostname_verify; +} + +static void +_efl_net_socket_ssl_hostname_verify_set(Eo *o EINA_UNUSED, Efl_Net_Socket_Ssl_Data *pd, Eina_Bool hostname_verify) +{ + pd->hostname_verify = hostname_verify; + if (!efl_finalized_get(o)) return; + + efl_net_ssl_conn_hostname_verify_set(&pd->ssl_conn, pd->hostname_verify); +} + +static const char * +_efl_net_socket_ssl_hostname_override_get(Eo *o EINA_UNUSED, Efl_Net_Socket_Ssl_Data *pd) +{ + return pd->hostname_override; +} + +static void +_efl_net_socket_ssl_hostname_override_set(Eo *o EINA_UNUSED, Efl_Net_Socket_Ssl_Data *pd, const char* hostname_override) +{ + char *tmp = NULL; + const char *hostname; + + eina_stringshare_replace(&pd->hostname_override, hostname_override); + hostname = pd->hostname_override; + if (!hostname) + { + const char *remote_address = (efl_isa(pd->sock, EFL_NET_DIALER_INTERFACE) ? + efl_net_dialer_address_dial_get(pd->sock) : + efl_net_socket_address_remote_get(pd->sock)); + if (remote_address) + { + const char *host, *port; + + tmp = strdup(remote_address); + EINA_SAFETY_ON_NULL_RETURN(tmp); + if (efl_net_ip_port_split(tmp, &host, &port)) + hostname = host; + } + } + + efl_net_ssl_conn_hostname_override_set(&pd->ssl_conn, hostname); + free(tmp); +} + +EOLIAN static Efl_Object * +_efl_net_socket_ssl_efl_object_finalize(Eo *o, Efl_Net_Socket_Ssl_Data *pd EINA_UNUSED) +{ + Eina_Error err; + + o = efl_finalize(efl_super(o, MY_CLASS)); + if (!o) return NULL; + + if (!pd->sock) + { + ERR("no Efl.Net.Socket was adopted by this SSL=%p", o); + return NULL; + } + + if (efl_isa(pd->sock, EFL_NET_DIALER_INTERFACE)) + { + if (!efl_net_dialer_connected_get(pd->sock)) + return o; + } + + err = efl_net_ssl_conn_handshake(&pd->ssl_conn, &pd->did_handshake); + if (err) + { + WRN("SSL=%p failed handshake", o); + return NULL; + } + + return o; +} + +EOLIAN static Eo * +_efl_net_socket_ssl_efl_object_constructor(Eo *o, Efl_Net_Socket_Ssl_Data *pd) +{ + pd->hostname_verify = 0xff; + pd->verify_mode = 0xff; + return efl_constructor(efl_super(o, MY_CLASS)); +} + +EOLIAN static void +_efl_net_socket_ssl_efl_object_destructor(Eo *o, Efl_Net_Socket_Ssl_Data *pd) +{ + if (efl_io_closer_close_on_destructor_get(o) && + (!efl_io_closer_closed_get(o))) + efl_io_closer_close(o); + + efl_destructor(efl_super(o, MY_CLASS)); + + efl_net_ssl_conn_teardown(&pd->ssl_conn); + if (pd->sock) + { + efl_event_callback_array_del(pd->sock, efl_net_socket_ssl_sock_cbs(), o); + if (efl_isa(pd->sock, EFL_NET_DIALER_INTERFACE)) + { + efl_event_callback_del(pd->sock, EFL_NET_DIALER_EVENT_CONNECTED, efl_net_socket_ssl_sock_connected, o); + } + efl_unref(pd->sock); + pd->sock = NULL; + } + + if (pd->context) + { + efl_event_callback_del(pd->context, EFL_EVENT_DEL, _efl_net_socket_ssl_context_del, o); + efl_unref(pd->context); + pd->context = NULL; + } + + eina_stringshare_replace(&pd->hostname_override, NULL); +} + +EOLIAN static Eina_Error +_efl_net_socket_ssl_efl_io_closer_close(Eo *o, Efl_Net_Socket_Ssl_Data *pd) +{ + EINA_SAFETY_ON_NULL_RETURN_VAL(pd->sock, EBADF); + + efl_io_reader_can_read_set(o, EINA_FALSE); + efl_io_reader_eos_set(o, EINA_TRUE); + efl_net_ssl_conn_teardown(&pd->ssl_conn); + if (efl_io_closer_closed_get(pd->sock)) + return 0; + return efl_io_closer_close(pd->sock); +} + +EOLIAN static Eina_Bool +_efl_net_socket_ssl_efl_io_closer_closed_get(Eo *o EINA_UNUSED, Efl_Net_Socket_Ssl_Data *pd) +{ + return (!pd->sock) || efl_io_closer_closed_get(pd->sock); +} + +EOLIAN static Eina_Error +_efl_net_socket_ssl_efl_io_reader_read(Eo *o, Efl_Net_Socket_Ssl_Data *pd, Eina_Rw_Slice *rw_slice) +{ + Eina_Error err; + + EINA_SAFETY_ON_NULL_RETURN_VAL(rw_slice, EINVAL); + + if (!pd->did_handshake) + { + rw_slice->mem = NULL; + rw_slice->len = 0; + return EAGAIN; + } + + err = efl_net_ssl_conn_read(&pd->ssl_conn, rw_slice); + + if (rw_slice->len == 0) + { + efl_io_reader_can_read_set(o, EINA_FALSE); + if (err == 0) + efl_io_reader_eos_set(o, EINA_TRUE); + } + + return err; +} + +EOLIAN static void +_efl_net_socket_ssl_efl_io_reader_can_read_set(Eo *o, Efl_Net_Socket_Ssl_Data *pd, Eina_Bool can_read) +{ + EINA_SAFETY_ON_NULL_RETURN(pd->sock); + if (pd->can_read == can_read) return; + pd->can_read = can_read; + efl_event_callback_call(o, EFL_IO_READER_EVENT_CAN_READ_CHANGED, NULL); +} + +EOLIAN static Eina_Bool +_efl_net_socket_ssl_efl_io_reader_can_read_get(Eo *o EINA_UNUSED, Efl_Net_Socket_Ssl_Data *pd) +{ + return pd->can_read; +} + +EOLIAN static Eina_Bool +_efl_net_socket_ssl_efl_io_reader_eos_get(Eo *o EINA_UNUSED, Efl_Net_Socket_Ssl_Data *pd) +{ + return pd->eos; +} + +EOLIAN static void +_efl_net_socket_ssl_efl_io_reader_eos_set(Eo *o, Efl_Net_Socket_Ssl_Data *pd, Eina_Bool is_eos) +{ + EINA_SAFETY_ON_NULL_RETURN(pd->sock); + if (pd->eos == is_eos) return; + pd->eos = is_eos; + if (is_eos) + efl_event_callback_call(o, EFL_IO_READER_EVENT_EOS, NULL); +} + +EOLIAN static Eina_Error +_efl_net_socket_ssl_efl_io_writer_write(Eo *o, Efl_Net_Socket_Ssl_Data *pd, Eina_Slice *ro_slice, Eina_Slice *remaining) +{ + Eina_Error err; + + EINA_SAFETY_ON_NULL_RETURN_VAL(ro_slice, EINVAL); + + if (!pd->did_handshake) + { + if (remaining) *remaining = *ro_slice; + ro_slice->mem = NULL; + ro_slice->len = 0; + return EAGAIN; + } + + if (remaining) *remaining = *ro_slice; + + err = efl_net_ssl_conn_write(&pd->ssl_conn, ro_slice); + + if (remaining) + { + remaining->bytes += ro_slice->len; + remaining->len -= ro_slice->len; + } + + if (ro_slice->len == 0) + efl_io_writer_can_write_set(o, EINA_FALSE); + + return err; +} + +EOLIAN static void +_efl_net_socket_ssl_efl_io_writer_can_write_set(Eo *o, Efl_Net_Socket_Ssl_Data *pd, Eina_Bool can_write) +{ + EINA_SAFETY_ON_NULL_RETURN(pd->sock); + if (pd->can_write == can_write) return; + pd->can_write = can_write; + efl_event_callback_call(o, EFL_IO_WRITER_EVENT_CAN_WRITE_CHANGED, NULL); +} + +EOLIAN static Eina_Bool +_efl_net_socket_ssl_efl_io_writer_can_write_get(Eo *o EINA_UNUSED, Efl_Net_Socket_Ssl_Data *pd) +{ + return pd->can_write; +} + +EOLIAN static Eina_Bool +_efl_net_socket_ssl_efl_io_closer_close_on_exec_set(Eo *o EINA_UNUSED, Efl_Net_Socket_Ssl_Data *pd, Eina_Bool close_on_exec) +{ + if (pd->sock) efl_io_closer_close_on_exec_set(pd->sock, close_on_exec); + return EINA_TRUE; +} + +EOLIAN static Eina_Bool +_efl_net_socket_ssl_efl_io_closer_close_on_exec_get(Eo *o EINA_UNUSED, Efl_Net_Socket_Ssl_Data *pd) +{ + return pd->sock && efl_io_closer_close_on_exec_get(pd->sock); +} + +EOLIAN static void +_efl_net_socket_ssl_efl_io_closer_close_on_destructor_set(Eo *o EINA_UNUSED, Efl_Net_Socket_Ssl_Data *pd, Eina_Bool close_on_destructor) +{ + if (pd->sock) efl_io_closer_close_on_destructor_set(pd->sock, close_on_destructor); +} + +EOLIAN static Eina_Bool +_efl_net_socket_ssl_efl_io_closer_close_on_destructor_get(Eo *o EINA_UNUSED, Efl_Net_Socket_Ssl_Data *pd) +{ + return pd->sock && efl_io_closer_close_on_destructor_get(pd->sock); +} + +EOLIAN static const char * +_efl_net_socket_ssl_efl_net_socket_address_local_get(Eo *o EINA_UNUSED, Efl_Net_Socket_Ssl_Data *pd) +{ + if (!pd->sock) return "unbound"; + return efl_net_socket_address_local_get(pd->sock); +} + +EOLIAN static const char * +_efl_net_socket_ssl_efl_net_socket_address_remote_get(Eo *o EINA_UNUSED, Efl_Net_Socket_Ssl_Data *pd) +{ + if (!pd->sock) return "unbound"; + return efl_net_socket_address_remote_get(pd->sock); +} + +#include "efl_net_socket_ssl.eo.c" diff --git a/src/lib/ecore_con/efl_net_socket_ssl.eo b/src/lib/ecore_con/efl_net_socket_ssl.eo new file mode 100644 index 0000000..0dd60e1 --- /dev/null +++ b/src/lib/ecore_con/efl_net_socket_ssl.eo @@ -0,0 +1,90 @@ +var Efl.Net.Socket.Ssl.Error.HANDSHAKE: Eina.Error; [[Failed SSL handshake]] +var Efl.Net.Socket.Ssl.Error.CERTIFICATE_VERIFY_FAILED: Eina.Error; [[Failed to verify peer's certificate]] + +class Efl.Net.Socket.Ssl (Efl.Object, Efl.Net.Socket) { + [[A wrapper socket doing SSL (Secure Sockets Layer). + + Use this wrapper around an existing socket to do secure + communication, a common use is to apply it to TCP sockets + created with @Efl.Net.Dialer.Tcp or @Efl.Net.Server.Tcp created + with "client,add" event. + + @since 1.19 + ]] + + events { + ssl,ready; [[Notifies the SSL handshake was done and the socket is now able to communicate]] + ssl,error: Eina.Error; [[an error such as @Efl.Net.Socket.Ssl.Error.HANDSHAKE]] + } + + methods { + adopt { + [[Adopt an Efl.Net.Dialer or regular Efl.Net.Socket that will be used for the actual communication. + + If used with an Efl.Net.Dialer object, it will assume + the 'connect' role, otherwise will use 'accept'. + + This is a constructor only method and should be called + before @Efl.Object.finalize. + ]] + params { + efl_net_socket: Efl.Net.Socket; [[The socket to adopt]] + ctx: Efl.Net.Ssl.Context; [[The SSL context to use when adopting the socket]] + } + } + + @property verify_mode { + [[How to verify the remote peer.]] + values { + verify_mode: Efl.Net.Ssl.Verify_Mode; + } + } + + @property hostname_verify { + [[Define if hostname should be verified. + + This will check the socket hostname (without the port in + case of an IP) or the overriden value from + @.hostname_override. + ]] + values { + hostname_verify: bool; + } + } + + @property hostname_override { + [[Overrides the hostname to use for this socket. + + Most of time this is useful if you're using an IP + address but the server certificate only specifies DNS + (names). + + If NULL, then it will fetch from socket using + @Efl.Net.Socket.address_remote or + @Efl.Net.Dialer.address_dial. + + It's only used if @.hostname_verify is $true. + ]] + values { + hostname_override: string @nullable; + } + } + } + + implements { + Efl.Object.constructor; + Efl.Object.destructor; + Efl.Object.finalize; + Efl.Io.Closer.close; + Efl.Io.Closer.closed.get; + Efl.Io.Closer.close_on_exec; + Efl.Io.Closer.close_on_destructor; + Efl.Io.Reader.read; + Efl.Io.Reader.can_read; + Efl.Io.Reader.eos; + Efl.Io.Writer.write; + Efl.Io.Writer.can_write; + Efl.Net.Socket.address_remote.get; + Efl.Net.Socket.address_local.get; + } +} diff --git a/src/lib/ecore_con/efl_net_ssl_conn-gnutls.c b/src/lib/ecore_con/efl_net_ssl_conn-gnutls.c new file mode 100644 index 0000000..c3601ed --- /dev/null +++ b/src/lib/ecore_con/efl_net_ssl_conn-gnutls.c @@ -0,0 +1,362 @@ +#include <gnutls/gnutls.h> +#include <gnutls/x509.h> + +struct _Efl_Net_Ssl_Conn { + gnutls_session_t session; + gnutls_datum_t ticket; + Eo *sock; + const char *hostname; + Efl_Net_Ssl_Verify_Mode verify_mode; + Eina_Bool hostname_verify; + Eina_Bool is_dialer; +}; + +static ssize_t +_efl_net_ssl_conn_write(gnutls_transport_ptr_t transp, const void *buf, size_t len) +{ + Eina_Slice slice = { + .mem = buf, + .len = len + }; + Efl_Net_Ssl_Conn *conn = transp; + Eina_Error err; + + if ((!buf) || (len == 0)) return 0; + if (!conn) return 0; + + if (!efl_io_writer_can_write_get(conn->sock)) + { + DBG("socket=%p would block if written!", conn->sock); + gnutls_transport_set_errno(conn->session, EAGAIN); + return -1; + } + + err = efl_io_writer_write(conn->sock, &slice, NULL); + if (err) + { + gnutls_transport_set_errno(conn->session, err); + return -1; + } + + gnutls_transport_set_errno(conn->session, 0); + return slice.len; +} + +static ssize_t +_efl_net_ssl_conn_read(gnutls_transport_ptr_t transp, void *buf, size_t len) +{ + Eina_Rw_Slice slice = { + .mem = buf, + .len = len + }; + Efl_Net_Ssl_Conn *conn = transp; + Eina_Error err; + + if ((!buf) || (len == 0)) return 0; + if (!conn) return 0; + + if (!efl_io_reader_can_read_get(conn->sock)) + { + DBG("socket=%p would block if read!", conn->sock); + gnutls_transport_set_errno(conn->session, EAGAIN); + return -1; + } + + err = efl_io_reader_read(conn->sock, &slice); + if (err) + { + gnutls_transport_set_errno(conn->session, err); + return -1; + } + + gnutls_transport_set_errno(conn->session, 0); + return slice.len; +} + +static Eina_Error +efl_net_ssl_conn_setup(Efl_Net_Ssl_Conn *conn, Eina_Bool is_dialer, Efl_Net_Socket *sock, Efl_Net_Ssl_Context *context) +{ + gnutls_certificate_request_t req; + int r; + + EINA_SAFETY_ON_TRUE_RETURN_VAL(conn->session != NULL, EALREADY); + + conn->is_dialer = is_dialer; + + conn->session = efl_net_ssl_context_connection_new(context); + EINA_SAFETY_ON_NULL_RETURN_VAL(conn->session, ENOSYS); + + gnutls_handshake_set_private_extensions(conn->session, 1); + + switch (conn->verify_mode) + { + case EFL_NET_SSL_VERIFY_MODE_NONE: + req = GNUTLS_CERT_IGNORE; + break; + case EFL_NET_SSL_VERIFY_MODE_OPTIONAL: + req = GNUTLS_CERT_REQUEST; + break; + case EFL_NET_SSL_VERIFY_MODE_REQUIRED: + default: + req = GNUTLS_CERT_REQUIRE; + } + gnutls_certificate_server_set_request(conn->session, req); + + if (is_dialer) + { + r = gnutls_session_ticket_enable_client(conn->session); + if (r < 0) + { + ERR("ssl_conn=%p could not enable session's ticket client: %s", conn, gnutls_strerror(r)); + goto error; + } + } + else + { + r = gnutls_session_ticket_key_generate(&conn->ticket); + if (r < 0) + { + ERR("ssl_conn=%p could not generate session ticket: %s", conn, gnutls_strerror(r)); + goto error; + } + + r = gnutls_session_ticket_enable_server(conn->session, &conn->ticket); + if (r < 0) + { + ERR("ssl_conn=%p could not enable session's ticket server: %s", conn, gnutls_strerror(r)); + goto error_ticket; + } + } + + conn->sock = sock; + gnutls_transport_set_ptr(conn->session, conn); + gnutls_transport_set_push_function(conn->session, _efl_net_ssl_conn_write); + gnutls_transport_set_pull_function(conn->session, _efl_net_ssl_conn_read); + + return 0; + + error_ticket: + gnutls_free(conn->ticket.data); + conn->ticket.data = NULL; + + error: + gnutls_deinit(conn->session); + conn->session = NULL; + return ENOSYS; +} + +static void +efl_net_ssl_conn_teardown(Efl_Net_Ssl_Conn *conn) +{ + if (conn->session) + { + gnutls_bye(conn->session, GNUTLS_SHUT_RDWR); + gnutls_deinit(conn->session); + conn->session = NULL; + } + + if (conn->ticket.data) + { + gnutls_free(conn->ticket.data); + conn->ticket.data = NULL; + } + + eina_stringshare_replace(&conn->hostname, NULL); +} + +static Eina_Error +efl_net_ssl_conn_write(Efl_Net_Ssl_Conn *conn, Eina_Slice *slice) +{ + ssize_t r = gnutls_record_send(conn->session, slice->mem, slice->len); + if (r < 0) + { + slice->len = 0; + if (gnutls_error_is_fatal(r)) + { + ERR("ssl_conn=%p could not send %zd bytes: %s", conn, slice->len, gnutls_strerror(r)); + return EINVAL; + } + DBG("ssl_conn=%p could not send %zd bytes: %s", conn, slice->len, gnutls_strerror(r)); + return EAGAIN; + } + slice->len = r; + return 0; +} + +static Eina_Error +efl_net_ssl_conn_read(Efl_Net_Ssl_Conn *conn, Eina_Rw_Slice *slice) +{ + ssize_t r = gnutls_record_recv(conn->session, slice->mem, slice->len); + if (r < 0) + { + slice->len = 0; + if (gnutls_error_is_fatal(r)) + { + ERR("ssl_conn=%p could not receive %zd bytes: %s", conn, slice->len, gnutls_strerror(r)); + return EINVAL; + } + DBG("ssl_conn=%p could not receive %zd bytes: %s", conn, slice->len, gnutls_strerror(r)); + return EAGAIN; + } + slice->len = r; + return 0; +} + +static Eina_Error +_efl_net_ssl_conn_verify(Efl_Net_Ssl_Conn *conn) +{ + unsigned status = 0; + int r; + + r = gnutls_certificate_verify_peers2(conn->session, &status); + if (r < 0) + { + ERR("ssl_conn=%p could not verify peer: %s", conn, gnutls_strerror(r)); + return EFL_NET_SOCKET_SSL_ERROR_HANDSHAKE; + } + + if (!status) return 0; + + if (status & GNUTLS_CERT_INVALID) + WRN("ssl_conn=%p The certificate is not trusted.", conn); + if (status & GNUTLS_CERT_SIGNER_NOT_FOUND) + WRN("ssl_conn=%p The certificate hasn't got a known issuer.", conn); + if (status & GNUTLS_CERT_REVOKED) + WRN("ssl_conn=%p The certificate has been revoked.", conn); + if (status & GNUTLS_CERT_EXPIRED) + WRN("ssl_conn=%p The certificate has expired", conn); + if (status & GNUTLS_CERT_NOT_ACTIVATED) + WRN("ssl_conn=%p The certificate is not yet activated", conn); + + return EFL_NET_SOCKET_SSL_ERROR_CERTIFICATE_VERIFY_FAILED; +} + +static Eina_Error +_efl_net_ssl_conn_hostname_verify(Efl_Net_Ssl_Conn *conn) +{ + const gnutls_datum_t *list; + unsigned int size; + gnutls_x509_crt_t cert = NULL; + int r; + + if ((!conn->hostname) || (conn->hostname[0] == '\0')) + { + ERR("ssl_conn=%p no hostname, cannot verify", conn); + return EFL_NET_SOCKET_SSL_ERROR_CERTIFICATE_VERIFY_FAILED; + } + + if (gnutls_certificate_type_get(conn->session) != GNUTLS_CRT_X509) + { + ERR("ssl_conn=%p PGP certificates are not yet supported!", conn); + return EFL_NET_SOCKET_SSL_ERROR_CERTIFICATE_VERIFY_FAILED; + } + + list = gnutls_certificate_get_peers(conn->session, &size); + if (!list) + { + ERR("ssl_conn=%p no peer certificate!", conn); + return EFL_NET_SOCKET_SSL_ERROR_HANDSHAKE; + } + + r = gnutls_x509_crt_init(&cert); + EINA_SAFETY_ON_TRUE_RETURN_VAL(r < 0, EFL_NET_SOCKET_SSL_ERROR_CERTIFICATE_VERIFY_FAILED); + + r = gnutls_x509_crt_import(cert, &list[0], GNUTLS_X509_FMT_DER); + if (r < 0) + { + ERR("ssl_conn=%p could not import x509 certificate to verify: %s", conn, gnutls_strerror(r)); + gnutls_x509_crt_deinit(cert); + return EFL_NET_SOCKET_SSL_ERROR_CERTIFICATE_VERIFY_FAILED; + } + + r = gnutls_x509_crt_check_hostname(cert, conn->hostname); + gnutls_x509_crt_deinit(cert); + + if (r == 1) + return 0; + + ERR("ssl_conn=%p hostname='%s' doesn't match certificate.", + conn, conn->hostname); + return EFL_NET_SOCKET_SSL_ERROR_CERTIFICATE_VERIFY_FAILED; +} + +static Eina_Error +efl_net_ssl_conn_handshake(Efl_Net_Ssl_Conn *conn, Eina_Bool *done) +{ + int r = gnutls_handshake(conn->session); + if (r < 0) + { + *done = EINA_FALSE; + if (gnutls_error_is_fatal(r)) + { + ERR("ssl_conn=%p could not handshake: %s", conn, gnutls_strerror(r)); + return EFL_NET_SOCKET_SSL_ERROR_HANDSHAKE; + } + + DBG("ssl_conn=%p did not finish handshake: %s", conn, gnutls_strerror(r)); + return 0; + } + + if (conn->verify_mode != EFL_NET_SSL_VERIFY_MODE_NONE) + { + Eina_Error err = _efl_net_ssl_conn_verify(conn); + if (err) + return err; + } + + if (conn->hostname_verify) + { + Eina_Error err = _efl_net_ssl_conn_hostname_verify(conn); + if (err) + return err; + } + + *done = EINA_TRUE; + DBG("ssl_conn=%p handshake finished!", conn); + return 0; +} + +static Eina_Error +efl_net_ssl_conn_verify_mode_set(Efl_Net_Ssl_Conn *conn, Efl_Net_Ssl_Verify_Mode verify_mode) +{ + gnutls_certificate_request_t req; + conn->verify_mode = verify_mode; + + switch (conn->verify_mode) + { + case EFL_NET_SSL_VERIFY_MODE_NONE: + req = GNUTLS_CERT_IGNORE; + break; + case EFL_NET_SSL_VERIFY_MODE_OPTIONAL: + req = GNUTLS_CERT_REQUEST; + break; + case EFL_NET_SSL_VERIFY_MODE_REQUIRED: + default: + req = GNUTLS_CERT_REQUIRE; + } + gnutls_certificate_server_set_request(conn->session, req); + + return 0; +} + +static Eina_Error +efl_net_ssl_conn_hostname_verify_set(Efl_Net_Ssl_Conn *conn, Eina_Bool hostname_verify) +{ + conn->hostname_verify = hostname_verify; + return 0; +} + +static Eina_Error +efl_net_ssl_conn_hostname_override_set(Efl_Net_Ssl_Conn *conn, const char *hostname) +{ + int r; + eina_stringshare_replace(&conn->hostname, hostname); + if (!hostname) hostname = ""; + r = gnutls_server_name_set(conn->session, GNUTLS_NAME_DNS, hostname, strlen(hostname)); + if (r < 0) + { + ERR("ssl_conn=%p could not set server name '%s': %s", conn, hostname, gnutls_strerror(r)); + return EINVAL; + } + return 0; +} diff --git a/src/lib/ecore_con/efl_net_ssl_conn-none.c b/src/lib/ecore_con/efl_net_ssl_conn-none.c new file mode 100644 index 0000000..e3e9292 --- /dev/null +++ b/src/lib/ecore_con/efl_net_ssl_conn-none.c @@ -0,0 +1,50 @@ +struct _Efl_Net_Ssl_Conn { +}; + +static Eina_Error +efl_net_ssl_conn_setup(Efl_Net_Ssl_Conn *conn EINA_UNUSED, Eina_Bool is_dialer EINA_UNUSED, Efl_Net_Socket *sock EINA_UNUSED, Efl_Net_Ssl_Context *context EINA_UNUSED) +{ + ERR("EFL compiled with --with-crypto=none"); + return ENOSYS; +} + +static void +efl_net_ssl_conn_teardown(Efl_Net_Ssl_Conn *conn EINA_UNUSED) +{ +} + +static Eina_Error +efl_net_ssl_conn_write(Efl_Net_Ssl_Conn *conn EINA_UNUSED, Eina_Slice *slice EINA_UNUSED) +{ + return ENOSYS; +} + +static Eina_Error +efl_net_ssl_conn_read(Efl_Net_Ssl_Conn *conn EINA_UNUSED, Eina_Rw_Slice *slice EINA_UNUSED) +{ + return ENOSYS; +} + +static Eina_Error +efl_net_ssl_conn_handshake(Efl_Net_Ssl_Conn *conn EINA_UNUSED, Eina_Bool *done EINA_UNUSED) +{ + return ENOSYS; +} + +static Eina_Error +efl_net_ssl_conn_verify_mode_set(Efl_Net_Ssl_Conn *conn EINA_UNUSED, Efl_Net_Ssl_Verify_Mode verify_mode EINA_UNUSED) +{ + return ENOSYS; +} + +static Eina_Error +efl_net_ssl_conn_hostname_verify_set(Efl_Net_Ssl_Conn *conn EINA_UNUSED, Eina_Bool hostname_verify EINA_UNUSED) +{ + return ENOSYS; +} + +static Eina_Error +efl_net_ssl_conn_hostname_override_set(Efl_Net_Ssl_Conn *conn EINA_UNUSED, const char *hostname EINA_UNUSED) +{ + return ENOSYS; +} diff --git a/src/lib/ecore_con/efl_net_ssl_conn-openssl.c b/src/lib/ecore_con/efl_net_ssl_conn-openssl.c new file mode 100644 index 0000000..88e73c7 --- /dev/null +++ b/src/lib/ecore_con/efl_net_ssl_conn-openssl.c @@ -0,0 +1,520 @@ +#include <openssl/x509v3.h> +#include <openssl/ssl.h> +#include <openssl/err.h> +#include <openssl/dh.h> + +#ifdef HAVE_ARPA_INET_H +# include <arpa/inet.h> +#endif + +/* OpenSSL's BIO is the abstraction for I/O, provide one for Efl.Io.* */ +static int +efl_net_socket_bio_create(BIO *b) +{ + b->init = 1; + b->num = 0; + b->ptr = NULL; + b->flags = 0; + return 1; +} + +static int +efl_net_socket_bio_destroy(BIO *b) +{ + if (!b) return 0; + b->init = 0; + b->ptr = NULL; + b->flags = 0; + return 1; +} + +static int +efl_net_socket_bio_read(BIO *b, char *buf, int len) +{ + Eina_Rw_Slice slice = { + .mem = buf, + .len = len + }; + Eo *sock = b->ptr; + Eina_Error err; + + if ((!buf) || (len <= 0)) return 0; + if (!sock) return 0; + + if (!efl_io_reader_can_read_get(sock)) + { + DBG("socket=%p would block if read!", sock); + BIO_set_retry_read(b); + return -1; + } + + err = efl_io_reader_read(sock, &slice); + BIO_clear_retry_flags(b); + if (err) + { + if (err == EAGAIN) + BIO_set_retry_write(b); + return -1; + } + + return slice.len; +} + +static int +efl_net_socket_bio_write(BIO *b, const char *buf, int len) +{ + Eina_Slice slice = { + .mem = buf, + .len = len + }; + Eo *sock = b->ptr; + Eina_Error err; + + if ((!buf) || (len <= 0)) return 0; + if (!sock) return 0; + + if (!efl_io_writer_can_write_get(sock)) + { + DBG("socket=%p would block if written!", sock); + BIO_set_retry_write(b); + return -1; + } + + err = efl_io_writer_write(sock, &slice, NULL); + BIO_clear_retry_flags(b); + if (err) + { + if (err == EAGAIN) + BIO_set_retry_write(b); + return -1; + } + + return slice.len; +} + +static long +efl_net_socket_bio_ctrl(BIO *b EINA_UNUSED, int cmd, long num EINA_UNUSED, void *ptr EINA_UNUSED) +{ + if (cmd == BIO_CTRL_FLUSH) + /* looks mandatory, but doesn't have a meaning here */ + return 1; + return 0; +} + +static int +efl_net_socket_bio_puts(BIO *b, const char *str) +{ + return efl_net_socket_bio_write(b, str, strlen(str)); +} + +static BIO_METHOD efl_net_socket_bio = { + 0x400, /* 0x400 means source & sink */ + "efl_net_socket wrapper", + efl_net_socket_bio_write, + efl_net_socket_bio_read, + efl_net_socket_bio_puts, + NULL, /* no gets */ + efl_net_socket_bio_ctrl, + efl_net_socket_bio_create, + efl_net_socket_bio_destroy +}; + +struct _Efl_Net_Ssl_Conn +{ + SSL *ssl; + BIO *bio; + const char *hostname; + Eina_Bool hostname_verify; + Eina_Bool did_certificates; +}; + +#define EFL_NET_SOCKET_SSL_CIPHERS "aRSA+HIGH:+kEDH:+kRSA:!kSRP:!kPSK:+3DES:!MD5" + +#define _efl_net_ssl_conn_session_debug(conn) \ + __efl_net_ssl_conn_session_debug(__FILE__, __LINE__, __FUNCTION__, conn) +static void +__efl_net_ssl_conn_session_debug(const char *file, int line, const char *fname, Efl_Net_Ssl_Conn *conn) +{ + STACK_OF(X509) * sk_X509; + STACK_OF(SSL_CIPHER) *sk_CIPHER; + + if (!eina_log_domain_level_check(_ecore_con_log_dom, EINA_LOG_LEVEL_DBG)) return; + + sk_X509 = SSL_get_peer_cert_chain(conn->ssl); + if (!sk_X509) + eina_log_print(_ecore_con_log_dom, EINA_LOG_LEVEL_DBG, file, fname, line, + "ssl_conn=%p No peer certificate chain", conn); + else + { + char subject[4096], issuer[4096]; + int i; + + eina_log_print(_ecore_con_log_dom, EINA_LOG_LEVEL_DBG, file, fname, line, + "ssl_conn=%p Peer Certificates:", conn); + for (i = 0; i < sk_X509_num(sk_X509); i++) + { + X509 *item = sk_X509_value(sk_X509, i); + eina_log_print(_ecore_con_log_dom, EINA_LOG_LEVEL_DBG, file, fname, line, + " #%02d %s (Issuer: %s)", + i, + X509_NAME_oneline(X509_get_subject_name(item), subject, sizeof(subject)), + X509_NAME_oneline(X509_get_issuer_name(item), issuer, sizeof(issuer))); + if (eina_log_domain_level_check(_ecore_con_log_dom, EINA_LOG_LEVEL_DBG + 1)) + X509_print_fp(stderr, item); + } + } + + sk_CIPHER = SSL_get_ciphers(conn->ssl); + if (!sk_CIPHER) + eina_log_print(_ecore_con_log_dom, EINA_LOG_LEVEL_DBG, file, fname, line, + "ssl_conn=%p No ciphers", conn); + else + { + char shared[8192]; + const SSL_CIPHER *cipher; + char *p; + int i; + + eina_log_print(_ecore_con_log_dom, EINA_LOG_LEVEL_DBG, file, fname, line, + "ssl_conn=%p Ciphers:", conn); + for (i = 0; i < sk_SSL_CIPHER_num(sk_CIPHER); i++) + { + cipher = sk_SSL_CIPHER_value(sk_CIPHER, i); + eina_log_print(_ecore_con_log_dom, EINA_LOG_LEVEL_DBG, file, fname, line, + " #%03d %s", i, SSL_CIPHER_get_name(cipher)); + } + + p = SSL_get_shared_ciphers(conn->ssl, shared, sizeof(shared)); + if (!p) + eina_log_print(_ecore_con_log_dom, EINA_LOG_LEVEL_DBG, file, fname, line, + "ssl_conn=%p No Client (Shared) Ciphers", conn); + else + { + eina_log_print(_ecore_con_log_dom, EINA_LOG_LEVEL_DBG, file, fname, line, + "ssl_conn=%p Client (Shared) Ciphers:", conn); + i = 0; + do + { + char *n = strchr(p, ':'); + if (n) + { + *n = '\0'; + n++; + } + + eina_log_print(_ecore_con_log_dom, EINA_LOG_LEVEL_DBG, file, fname, line, + " #%03d %s", i, p); + p = n; + i++; + } + while (p); + } + + cipher = SSL_get_current_cipher(conn->ssl); + if (!cipher) + eina_log_print(_ecore_con_log_dom, EINA_LOG_LEVEL_DBG, file, fname, line, + "ssl_conn=%p No cipher in use", conn); + else + eina_log_print(_ecore_con_log_dom, EINA_LOG_LEVEL_DBG, file, fname, line, + "ssl_conn=%p Current Cipher: %s (%s)", + conn, + SSL_CIPHER_get_version(cipher), + SSL_CIPHER_get_name(cipher)); + } + + if (eina_log_domain_level_check(_ecore_con_log_dom, EINA_LOG_LEVEL_DBG + 1)) + SSL_SESSION_print_fp(stderr, SSL_get_session(conn->ssl)); + + eina_log_print(_ecore_con_log_dom, EINA_LOG_LEVEL_DBG, file, fname, line, + "ssl_conn=%p end of SSL session information", conn); +} + +#define _efl_net_ssl_conn_check_errors() \ + __efl_net_ssl_conn_check_errors(__FILE__, __LINE__, __FUNCTION__) +static unsigned long +__efl_net_ssl_conn_check_errors(const char *file, int line, const char *fname) +{ + unsigned long first = 0; + do + { + const char *_ssl_err_file, *_ssl_err_data; + int _ssl_err_line, _ssl_err_flags; + unsigned long _ssl_err = ERR_get_error_line_data(&_ssl_err_file, &_ssl_err_line, &_ssl_err_data, &_ssl_err_flags); + if (!_ssl_err) break; + if (!first) first = _ssl_err; + eina_log_print(_ecore_con_log_dom, EINA_LOG_LEVEL_ERR, file, fname, line, + "OpenSSL error %s:%d%s%s: %s", + _ssl_err_file, _ssl_err_line, + (_ssl_err_flags & ERR_TXT_STRING) ? " " : "", + (_ssl_err_flags & ERR_TXT_STRING) ? _ssl_err_data : "", + ERR_reason_error_string(_ssl_err)); + } + while (1); + return first; +} + +static Eina_Error +efl_net_ssl_conn_setup(Efl_Net_Ssl_Conn *conn, Eina_Bool is_dialer, Efl_Net_Socket *sock, Efl_Net_Ssl_Context *context) +{ + char vbuf[32]; + const char *ssl_ver_str = NULL; + int ssl_ver; + static const struct { + int ver; + const char *str; + } *ssl_ver_itr, ssl_ver_map[] = { + {SSL3_VERSION, "SSLv3.0"}, + {TLS1_VERSION, "TLSv1.0"}, + {TLS1_1_VERSION, "TLSv1.1"}, + {TLS1_2_VERSION, "TLSv1.2"}, + {DTLS1_VERSION, "DTLSv1.0"}, + {DTLS1_2_VERSION, "DTLSv1.2"}, + {DTLS1_BAD_VER, "DTLSv1.0"}, + {0, NULL} + }; + + EINA_SAFETY_ON_TRUE_RETURN_VAL(conn->ssl != NULL, EALREADY); + + conn->ssl = efl_net_ssl_context_connection_new(context); + EINA_SAFETY_ON_NULL_RETURN_VAL(conn->ssl, ENOSYS); + + conn->bio = BIO_new(&efl_net_socket_bio); + EINA_SAFETY_ON_NULL_GOTO(conn->bio, error_bio); + + conn->bio->ptr = sock; + + SSL_set_bio(conn->ssl, conn->bio, conn->bio); + if (is_dialer) + SSL_set_connect_state(conn->ssl); + else + SSL_set_accept_state(conn->ssl); + + ssl_ver = SSL_version(conn->ssl); + for (ssl_ver_itr = ssl_ver_map; ssl_ver_itr->str != NULL; ssl_ver_itr++) + { + if (ssl_ver_itr->ver == ssl_ver) + { + ssl_ver_str = ssl_ver_itr->str; + break; + } + } + if (!ssl_ver_str) + { + snprintf(vbuf, sizeof(vbuf), "%#x", ssl_ver); + ssl_ver_str = vbuf; + } + DBG("Using SSL %s", ssl_ver_str); + + return 0; + + error_bio: + SSL_shutdown(conn->ssl); + SSL_free(conn->ssl); + conn->ssl = NULL; + return ENOSYS; +} + +static void +efl_net_ssl_conn_teardown(Efl_Net_Ssl_Conn *conn) +{ + if (conn->bio) + { + /* NOTE: no BIO_free() as it's done by SSL_free(). */ + } + + if (conn->ssl) + { + if (!SSL_shutdown(conn->ssl)) + SSL_shutdown(conn->ssl); + + SSL_free(conn->ssl); + conn->ssl = NULL; + } + + eina_stringshare_replace(&conn->hostname, NULL); +} + +static Eina_Error +efl_net_ssl_conn_write(Efl_Net_Ssl_Conn *conn, Eina_Slice *slice) +{ + int r = SSL_write(conn->ssl, slice->mem, slice->len); + if (r < 0) + { + int ssl_err = SSL_get_error(conn->ssl, r); + + slice->len = 0; + if (ssl_err == SSL_ERROR_WANT_WRITE) return EAGAIN; + _efl_net_ssl_conn_check_errors(); + ERR("ssl_conn=%p could not write", conn); + return EINVAL; + } + slice->len = r; + return 0; +} + +static Eina_Error +efl_net_ssl_conn_read(Efl_Net_Ssl_Conn *conn, Eina_Rw_Slice *slice) +{ + int r = SSL_read(conn->ssl, slice->mem, slice->len); + if (r < 0) + { + int ssl_err = SSL_get_error(conn->ssl, r); + + slice->len = 0; + if (ssl_err == SSL_ERROR_WANT_READ) return EAGAIN; + _efl_net_ssl_conn_check_errors(); + ERR("ssl_conn=%p could not read", conn); + return EINVAL; + } + slice->len = r; + return 0; +} + +static Eina_Error +_efl_net_ssl_conn_hostname_verify(Efl_Net_Ssl_Conn *conn) +{ + X509 *x509; + struct sockaddr_storage addr; + const char *label; + int family = AF_INET; + int r; + + if ((!conn->hostname) || (conn->hostname[0] == '\0')) + { + ERR("ssl_conn=%p no hostname, cannot verify", conn); + return EFL_NET_SOCKET_SSL_ERROR_CERTIFICATE_VERIFY_FAILED; + } + + x509 = SSL_get_peer_certificate(conn->ssl); + if (!x509) + { + ERR("ssl_conn=%p no peer certificate!", conn); + return EFL_NET_SOCKET_SSL_ERROR_HANDSHAKE; + } + + if (strchr(conn->hostname, ':')) family = AF_INET6; + if (inet_pton(family, conn->hostname, &addr) == 1) + { + label = "IP address"; + r = X509_check_ip_asc(x509, conn->hostname, 0); + } + else + { + label = "hostname"; + r = X509_check_host(x509, conn->hostname, 0, 0, NULL); + } + + if (r != 1) + { + ERR("ssl_conn=%p %s='%s' doesn't match certificate.", + conn, label, conn->hostname); + return EFL_NET_SOCKET_SSL_ERROR_CERTIFICATE_VERIFY_FAILED; + } + + DBG("ssl_conn=%p %s='%s' matches certificate.", conn, label, conn->hostname); + + return 0; +} + +static Eina_Error +efl_net_ssl_conn_handshake(Efl_Net_Ssl_Conn *conn, Eina_Bool *done) +{ + int r = SSL_do_handshake(conn->ssl); + long err_ssl; + const char *err_file; + const char *err_data; + int err_line, err_flags; + + *done = EINA_FALSE; + + if (r == 1) + { + _efl_net_ssl_conn_session_debug(conn); + + if (conn->hostname_verify) + { + Eina_Error err = _efl_net_ssl_conn_hostname_verify(conn); + if (err) + return err; + } + + *done = EINA_TRUE; + DBG("ssl_conn=%p handshake finished!", conn); + return 0; + } + + r = SSL_get_error(conn->ssl, r); + if ((r == SSL_ERROR_WANT_READ) || (r == SSL_ERROR_WANT_WRITE)) + { + DBG("ssl_conn=%p handshake needs more data...", conn); + return 0; + } + + err_ssl = ERR_peek_error_line_data(&err_file, &err_line, &err_data, &err_flags); + _efl_net_ssl_conn_check_errors(); + + if (!err_ssl) + DBG("ssl_conn=%p handshake error=%#x (SSL_ERROR_SSL=%#x)", conn, r, SSL_ERROR_SSL); + else + DBG("ssl_conn=%p handshake error=%#x (SSL_ERROR_SSL=%#x) [%s:%d%s%s %#lx='%s']", + conn, r, SSL_ERROR_SSL, + err_file, err_line, + (err_flags & ERR_TXT_STRING) ? " " : "", + (err_flags & ERR_TXT_STRING) ? err_data : "", + err_ssl, ERR_reason_error_string(err_ssl)); + if (r == SSL_ERROR_SSL) + { + _efl_net_ssl_conn_session_debug(conn); + + if ((ERR_GET_LIB(err_ssl) == ERR_LIB_SSL) && + (ERR_GET_REASON(err_ssl) == SSL_R_CERTIFICATE_VERIFY_FAILED)) + { + WRN("ssl_conn=%p certificate verification failed, handshake failed", conn); + return EFL_NET_SOCKET_SSL_ERROR_CERTIFICATE_VERIFY_FAILED; + } + WRN("ssl_conn=%p handshake failed: %s", conn, ERR_reason_error_string(err_ssl)); + return EFL_NET_SOCKET_SSL_ERROR_HANDSHAKE; + } + + return 0; +} + +static Eina_Error +efl_net_ssl_conn_verify_mode_set(Efl_Net_Ssl_Conn *conn, Efl_Net_Ssl_Verify_Mode verify_mode) +{ + int ssl_mode; + + switch (verify_mode) + { + case EFL_NET_SSL_VERIFY_MODE_NONE: + ssl_mode = SSL_VERIFY_NONE; + break; + case EFL_NET_SSL_VERIFY_MODE_OPTIONAL: + ssl_mode = SSL_VERIFY_PEER; + break; + case EFL_NET_SSL_VERIFY_MODE_REQUIRED: + ssl_mode = SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT; + break; + default: + ERR("unknown verify_mode=%d", verify_mode); + return EINVAL; + } + + SSL_set_verify(conn->ssl, ssl_mode, SSL_get_verify_callback(conn->ssl)); + return 0; +} + +static Eina_Error +efl_net_ssl_conn_hostname_verify_set(Efl_Net_Ssl_Conn *conn, Eina_Bool hostname_verify) +{ + conn->hostname_verify = hostname_verify; + return 0; +} + +static Eina_Error +efl_net_ssl_conn_hostname_override_set(Efl_Net_Ssl_Conn *conn, const char *hostname) +{ + eina_stringshare_replace(&conn->hostname, hostname); + if (hostname && (!conn->hostname)) return ENOMEM; + return 0; +} diff --git a/src/lib/ecore_con/efl_net_ssl_context.c b/src/lib/ecore_con/efl_net_ssl_context.c new file mode 100644 index 0000000..22c5ad8 --- /dev/null +++ b/src/lib/ecore_con/efl_net_ssl_context.c @@ -0,0 +1,373 @@ +#define EFL_NET_SSL_CONTEXT_PROTECTED 1 +#define EFL_IO_READER_PROTECTED 1 +#define EFL_IO_WRITER_PROTECTED 1 +#define EFL_IO_CLOSER_PROTECTED 1 + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif + +#include "Ecore.h" +#include "Ecore_Con.h" +#include "ecore_con_private.h" + +#include "Emile.h" + +/** + * This function is used by efl_net_socket_ssl to retrieve a new + * connection based on the implementation-depentent context. + * + * @internal + */ +void *efl_net_ssl_context_connection_new(Efl_Net_Ssl_Context *context); + +typedef struct _Efl_Net_Ssl_Ctx Efl_Net_Ssl_Ctx; + +typedef struct _Efl_Net_Ssl_Ctx_Config { + Efl_Net_Ssl_Cipher cipher; + Eina_Bool is_dialer; + Eina_Bool load_defaults; + Eina_List **certificates; + Eina_List **private_keys; + Eina_List **certificate_revogation_lists; + Eina_List **certificate_authorities; +} Efl_Net_Ssl_Ctx_Config; + +/** + * Returns the platform dependent context to efl_net_socket_ssl + * wrapper. + * + * @internal + */ +static void *efl_net_ssl_ctx_connection_new(Efl_Net_Ssl_Ctx *ctx); + +/** + * Setups the SSL context + * + * Update the given lists, removing invalid entries. If all entries + * failed in a list, return EINVAL. + * + * @internal + */ +static Eina_Error efl_net_ssl_ctx_setup(Efl_Net_Ssl_Ctx *ctx, Efl_Net_Ssl_Ctx_Config cfg); + +/** + * Cleans up the SSL associated to this context. + * @internal + */ +static void efl_net_ssl_ctx_teardown(Efl_Net_Ssl_Ctx *ctx); + +/** + * Configure how to verify peer. + * + * @internal + */ +static Eina_Error efl_net_ssl_ctx_verify_mode_set(Efl_Net_Ssl_Ctx *ctx, Efl_Net_Ssl_Verify_Mode verify_mode); + +/** + * Configure whenever to check for hostname. + * + * @internal + */ +static Eina_Error efl_net_ssl_ctx_hostname_verify_set(Efl_Net_Ssl_Ctx *ctx, Eina_Bool hostname_verify); + +/** + * Configure the hostname to use. + * + * @note duplicate hostname if needed! + * + * @internal + */ +static Eina_Error efl_net_ssl_ctx_hostname_set(Efl_Net_Ssl_Ctx *ctx, const char *hostname); + +#if HAVE_OPENSSL +#include "efl_net_ssl_ctx-openssl.c" +#elif HAVE_GNUTLS +#include "efl_net_ssl_ctx-gnutls.c" +#else +#include "efl_net_ssl_ctx-none.c" +#endif + +#define MY_CLASS EFL_NET_SSL_CONTEXT_CLASS + +typedef struct _Efl_Net_Ssl_Context_Data +{ + Efl_Net_Ssl_Ctx ssl_ctx; + Eina_List *certificates; + Eina_List *private_keys; + Eina_List *certificate_revogation_lists; + Eina_List *certificate_authorities; + const char *hostname; + Efl_Net_Ssl_Cipher cipher; + Eina_Bool is_dialer; + Efl_Net_Ssl_Verify_Mode verify_mode; + Eina_Bool load_defaults; + Eina_Bool hostname_verify; + Eina_Bool did_handshake; + Eina_Bool can_read; + Eina_Bool eos; + Eina_Bool can_write; +} Efl_Net_Ssl_Context_Data; + + +void * +efl_net_ssl_context_connection_new(Efl_Net_Ssl_Context *context) +{ + Efl_Net_Ssl_Context_Data *pd = efl_data_scope_get(context, MY_CLASS); + EINA_SAFETY_ON_NULL_RETURN_VAL(pd, NULL); + return efl_net_ssl_ctx_connection_new(&pd->ssl_ctx); +} + +EOLIAN static void +_efl_net_ssl_context_setup(Eo *o, Efl_Net_Ssl_Context_Data *pd, Efl_Net_Ssl_Cipher cipher, Eina_Bool is_dialer) +{ + EINA_SAFETY_ON_TRUE_RETURN(efl_finalized_get(o)); + EINA_SAFETY_ON_TRUE_RETURN(cipher > EFL_NET_SSL_CIPHER_TLSV1_2); + + pd->cipher = cipher; + pd->is_dialer = is_dialer; +} + +static Eina_List * +_efl_net_ssl_context_string_iter_to_list(Eina_Iterator *it) +{ + Eina_List *lst = NULL; + const char *str; + EINA_ITERATOR_FOREACH(it, str) + { + if (!str) continue; + lst = eina_list_append(lst, eina_stringshare_add(str)); + } + eina_iterator_free(it); + return lst; +} + +static void +_efl_net_ssl_context_string_list_free(Eina_List **p_lst) +{ + const char *str; + EINA_LIST_FREE(*p_lst, str) + eina_stringshare_del(str); +} + +static Eina_Iterator * +_efl_net_ssl_context_certificates_get(Eo *o EINA_UNUSED, Efl_Net_Ssl_Context_Data *pd) +{ + return eina_list_iterator_new(pd->certificates); +} + +static void +_efl_net_ssl_context_certificates_set(Eo *o EINA_UNUSED, Efl_Net_Ssl_Context_Data *pd, Eina_Iterator *it) +{ + _efl_net_ssl_context_string_list_free(&pd->certificates); + pd->certificates = _efl_net_ssl_context_string_iter_to_list(it); +} + +static Eina_Iterator * +_efl_net_ssl_context_private_keys_get(Eo *o EINA_UNUSED, Efl_Net_Ssl_Context_Data *pd) +{ + return eina_list_iterator_new(pd->private_keys); +} + +static void +_efl_net_ssl_context_private_keys_set(Eo *o EINA_UNUSED, Efl_Net_Ssl_Context_Data *pd, Eina_Iterator *it) +{ + _efl_net_ssl_context_string_list_free(&pd->private_keys); + pd->private_keys = _efl_net_ssl_context_string_iter_to_list(it); +} + +static Eina_Iterator * +_efl_net_ssl_context_certificate_revogation_lists_get(Eo *o EINA_UNUSED, Efl_Net_Ssl_Context_Data *pd) +{ + return eina_list_iterator_new(pd->certificate_revogation_lists); +} + +static void +_efl_net_ssl_context_certificate_revogation_lists_set(Eo *o EINA_UNUSED, Efl_Net_Ssl_Context_Data *pd, Eina_Iterator *it) +{ + _efl_net_ssl_context_string_list_free(&pd->certificate_revogation_lists); + pd->certificate_revogation_lists = _efl_net_ssl_context_string_iter_to_list(it); +} + +static Eina_Iterator * +_efl_net_ssl_context_certificate_authorities_get(Eo *o EINA_UNUSED, Efl_Net_Ssl_Context_Data *pd) +{ + return eina_list_iterator_new(pd->certificate_authorities); +} + +static void +_efl_net_ssl_context_certificate_authorities_set(Eo *o EINA_UNUSED, Efl_Net_Ssl_Context_Data *pd, Eina_Iterator *it) +{ + _efl_net_ssl_context_string_list_free(&pd->certificate_authorities); + pd->certificate_authorities = _efl_net_ssl_context_string_iter_to_list(it); +} + +static Eina_Bool +_efl_net_ssl_context_default_paths_load_get(Eo *o EINA_UNUSED, Efl_Net_Ssl_Context_Data *pd) +{ + return pd->load_defaults; +} + +static void +_efl_net_ssl_context_default_paths_load_set(Eo *o EINA_UNUSED, Efl_Net_Ssl_Context_Data *pd, Eina_Bool load_defaults) +{ + pd->load_defaults = load_defaults; +} + +static Efl_Net_Ssl_Verify_Mode +_efl_net_ssl_context_verify_mode_get(Eo *o EINA_UNUSED, Efl_Net_Ssl_Context_Data *pd) +{ + return pd->verify_mode; +} + +static void +_efl_net_ssl_context_verify_mode_set(Eo *o, Efl_Net_Ssl_Context_Data *pd, Efl_Net_Ssl_Verify_Mode verify_mode) +{ + pd->verify_mode = verify_mode; + if (!efl_finalized_get(o)) return; + + efl_net_ssl_ctx_verify_mode_set(&pd->ssl_ctx, pd->verify_mode); +} + +static Eina_Bool +_efl_net_ssl_context_hostname_verify_get(Eo *o EINA_UNUSED, Efl_Net_Ssl_Context_Data *pd) +{ + return pd->hostname_verify; +} + +static void +_efl_net_ssl_context_hostname_verify_set(Eo *o EINA_UNUSED, Efl_Net_Ssl_Context_Data *pd, Eina_Bool hostname_verify) +{ + pd->hostname_verify = hostname_verify; + if (!efl_finalized_get(o)) return; + + efl_net_ssl_ctx_hostname_verify_set(&pd->ssl_ctx, pd->hostname_verify); +} + +static const char * +_efl_net_ssl_context_hostname_get(Eo *o EINA_UNUSED, Efl_Net_Ssl_Context_Data *pd) +{ + return pd->hostname; +} + +static void +_efl_net_ssl_context_hostname_set(Eo *o EINA_UNUSED, Efl_Net_Ssl_Context_Data *pd, const char* hostname) +{ + eina_stringshare_replace(&pd->hostname, hostname); + if (!efl_finalized_get(o)) return; + + efl_net_ssl_ctx_hostname_set(&pd->ssl_ctx, pd->hostname); +} + +EOLIAN static Efl_Object * +_efl_net_ssl_context_efl_object_finalize(Eo *o, Efl_Net_Ssl_Context_Data *pd) +{ + Eina_Error err; + Efl_Net_Ssl_Ctx_Config cfg; + + o = efl_finalize(efl_super(o, MY_CLASS)); + if (!o) return NULL; + + if (!emile_cipher_init()) + { + ERR("could not initialize cipher subsystem."); + return NULL; + } + + if (pd->is_dialer) + { + if ((uint8_t)pd->verify_mode == 0xff) + pd->verify_mode = EFL_NET_SSL_VERIFY_MODE_REQUIRED; + if (pd->hostname_verify == 0xff) + pd->hostname_verify = EINA_TRUE; + if (pd->load_defaults == 0xff) + pd->load_defaults = EINA_TRUE; + } + else + { + cfg.is_dialer = EINA_FALSE; + if ((uint8_t)pd->verify_mode == 0xff) + pd->verify_mode = EFL_NET_SSL_VERIFY_MODE_NONE; + if (pd->hostname_verify == 0xff) + pd->hostname_verify = EINA_FALSE; + if (pd->load_defaults == 0xff) + pd->load_defaults = EINA_FALSE; + } + + cfg.cipher = pd->cipher; + cfg.is_dialer = pd->is_dialer; + cfg.load_defaults = pd->load_defaults; + cfg.certificates = &pd->certificates; + cfg.private_keys = &pd->private_keys; + cfg.certificate_revogation_lists = &pd->certificate_revogation_lists; + cfg.certificate_authorities = &pd->certificate_authorities; + cfg.load_defaults = pd->load_defaults; + + err = efl_net_ssl_ctx_setup(&pd->ssl_ctx, cfg); + if (err) + { + ERR("o=%p failed to setup context (is_dialer=%d)", o, cfg.is_dialer); + return NULL; + } + DBG("o=%p setup context (is_dialer=%d) ssl_ctx=%p", o, cfg.is_dialer, &pd->ssl_ctx); + + efl_net_ssl_ctx_verify_mode_set(&pd->ssl_ctx, pd->verify_mode); + efl_net_ssl_ctx_hostname_verify_set(&pd->ssl_ctx, pd->hostname_verify); + efl_net_ssl_ctx_hostname_set(&pd->ssl_ctx, pd->hostname); + + return o; +} + +EOLIAN static Eo * +_efl_net_ssl_context_efl_object_constructor(Eo *o, Efl_Net_Ssl_Context_Data *pd) +{ + pd->cipher = EFL_NET_SSL_CIPHER_AUTO; + pd->is_dialer = EINA_TRUE; + pd->load_defaults = 0xff; + pd->hostname_verify = 0xff; + pd->verify_mode = 0xff; + return efl_constructor(efl_super(o, MY_CLASS)); +} + +EOLIAN static void +_efl_net_ssl_context_efl_object_destructor(Eo *o, Efl_Net_Ssl_Context_Data *pd) +{ + efl_destructor(efl_super(o, MY_CLASS)); + + efl_net_ssl_ctx_teardown(&pd->ssl_ctx); + + _efl_net_ssl_context_string_list_free(&pd->certificates); + _efl_net_ssl_context_string_list_free(&pd->private_keys); + _efl_net_ssl_context_string_list_free(&pd->certificate_revogation_lists); + _efl_net_ssl_context_string_list_free(&pd->certificate_authorities); + + eina_stringshare_replace(&pd->hostname, NULL); +} + +static Efl_Net_Ssl_Context *_efl_net_ssl_context_default_dialer = NULL; + +static void +_efl_net_ssl_context_default_dialer_del(void *data EINA_UNUSED, const Efl_Event *event EINA_UNUSED) +{ + _efl_net_ssl_context_default_dialer = NULL; +} + +EOLIAN static Efl_Net_Ssl_Context * +_efl_net_ssl_context_default_dialer_get(Efl_Class *klass, void *pd EINA_UNUSED) +{ + if (!_efl_net_ssl_context_default_dialer) + { + _efl_net_ssl_context_default_dialer = efl_add(klass, NULL, + efl_net_ssl_context_verify_mode_set(efl_added, EFL_NET_SSL_VERIFY_MODE_REQUIRED), + efl_net_ssl_context_hostname_verify_set(efl_added, EINA_TRUE), + efl_net_ssl_context_default_paths_load_set(efl_added, EINA_TRUE), + efl_net_ssl_context_setup(efl_added, EFL_NET_SSL_CIPHER_AUTO, EINA_TRUE)); + efl_event_callback_add(_efl_net_ssl_context_default_dialer, + EFL_EVENT_DEL, + _efl_net_ssl_context_default_dialer_del, + NULL); + } + return _efl_net_ssl_context_default_dialer; +} + +#include "efl_net_ssl_context.eo.c" diff --git a/src/lib/ecore_con/efl_net_ssl_context.eo b/src/lib/ecore_con/efl_net_ssl_context.eo new file mode 100644 index 0000000..3c8caec --- /dev/null +++ b/src/lib/ecore_con/efl_net_ssl_context.eo @@ -0,0 +1,122 @@ +import efl_net_ssl_types; + +class Efl.Net.Ssl.Context (Efl.Object) { + [[A SSL Context that is used to start a SSL socket wrapper. + + The context will contain common configurations such as + certificates, private keys, certificate revogation lists (CRLs), + certificate authorities (CAs) and so on. + + The method @.setup must be called once before + @Efl.Object.finalize in order to define the mandatory + operational parameters. + + \@note All setter methods must be called before @Efl.Object.finalize. + + @since 1.19 + ]] + + methods { + @property default_dialer @class { + [[The default context for dialers. + + It will start with: + + - default_paths_load = true + - cipher = auto + - verify_mode = required + - verify_hostname = true + + ]] + get { } + values { + default_client_context: Efl.Net.Ssl.Context; + } + } + + setup { + [[Defines the context mandatory operation parameters]] + params { + cipher: Efl.Net.Ssl.Cipher; [[Cipher to use, prefer @Efl.Net.Ssl.Cipher.auto]] + is_dialer: bool; [[If $true, this SSL context is targeted at dialers connecting to a remote serer]] + } + } + + @property certificates { + [[The list of paths to certificates to use.]] + values { + paths: free(own(iterator<string>), eina_iterator_free); + } + } + + @property private_keys { + [[The list of paths to private keys to use.]] + values { + paths: free(own(iterator<string>), eina_iterator_free); + } + } + + @property certificate_revogation_lists { + [[The list of paths to CRL (certificate revogation list) to use.]] + values { + paths: free(own(iterator<string>), eina_iterator_free); + } + } + + @property certificate_authorities { + [[The list of paths to CA (certificate authoritie) to use.]] + values { + paths: free(own(iterator<string>), eina_iterator_free); + } + } + + @property default_paths_load { + [[If $true, will use system's default certificate storage]] + values { + default_paths_load: bool; + } + } + + @property verify_mode { + [[How to verify the remote peer.]] + values { + verify_mode: Efl.Net.Ssl.Verify_Mode; + } + } + + @property hostname_verify { + [[Define if hostname should be verified. + + This will check the socket hostname (without the port in + case of an IP) or the overriden value from + @.hostname. + ]] + values { + hostname_verify: bool; + } + } + + @property hostname { + [[Defines the hostname to use for sockets. + + This is useful to avoid replicating a hostname in all + socket wrapper with hostname_override. + + If NULL, then sockets wrappers will will fetch from + adopted socket using address_remote or + address_dial. + + It's only used if @.hostname_verify is $true. + ]] + values { + hostname: string @nullable; + } + } + } + + implements { + Efl.Object.constructor; + Efl.Object.destructor; + Efl.Object.finalize; + } +} diff --git a/src/lib/ecore_con/efl_net_ssl_ctx-gnutls.c b/src/lib/ecore_con/efl_net_ssl_ctx-gnutls.c new file mode 100644 index 0000000..cdd3969 --- /dev/null +++ b/src/lib/ecore_con/efl_net_ssl_ctx-gnutls.c @@ -0,0 +1,310 @@ +#include <gnutls/gnutls.h> + +struct _Efl_Net_Ssl_Ctx { + gnutls_certificate_credentials_t x509_cred; + gnutls_priority_t priority; + Eina_Bool is_dialer; +}; + +static Eina_Error +_efl_net_ssl_ctx_load_lists(Efl_Net_Ssl_Ctx *ctx, Efl_Net_Ssl_Ctx_Config cfg) +{ + Eina_List *n, *n_next, *pk_node; + const char *path; + unsigned certificates_count = eina_list_count(*cfg.certificates); + unsigned private_keys_count = eina_list_count(*cfg.private_keys); + unsigned certificate_revogation_lists_count = eina_list_count(*cfg.certificate_revogation_lists); + unsigned certificate_authorities_count = eina_list_count(*cfg.certificate_authorities); + int r; + + ctx->is_dialer = cfg.is_dialer; + + if (cfg.load_defaults) + { + r = gnutls_certificate_set_x509_system_trust(ctx->x509_cred); + if (r < 0) + { + ERR("ssl_ctx=%p could not load default paths: %s", ctx, gnutls_strerror(r)); + return ENOSYS; + } + DBG("ssl_ctx=%p loaded default paths", ctx); + } + else + DBG("ssl_ctx=%p did not load default paths", ctx); + + /* GNUTLS needs certificate-key pairs, so we do: + * + * - if no private keys, use certificate as its own key; + * + * - if a private keys, walk the list alongside certificates, but + * do NOT delete elements if list sizes are different. Stop at + * last private key, allowing a single private key for multiple + * certificates. + */ + pk_node = *cfg.private_keys; + EINA_LIST_FOREACH_SAFE(*cfg.certificates, n, n_next, path) + { + const char *key = pk_node ? pk_node->data : path; + + r = gnutls_certificate_set_x509_key_file(ctx->x509_cred, path, key, GNUTLS_X509_FMT_PEM); + if (r < 0) + { + ERR("ssl_ctx=%p could not use certificate from '%s' with key '%s': %s", + ctx, path, key, gnutls_strerror(r)); + + if (pk_node) + { + if (eina_list_count(*cfg.private_keys) == eina_list_count(*cfg.certificates)) + { + pk_node = pk_node->next; + eina_stringshare_del(key); + *cfg.private_keys = eina_list_remove_list(*cfg.private_keys, pk_node->prev); + } + else if (pk_node->next) pk_node = pk_node->next; + } + + eina_stringshare_del(path); + *cfg.certificates = eina_list_remove_list(*cfg.certificates, n); + continue; + } + else + { + if (pk_node->next) pk_node = pk_node->next; + } + + DBG("ssl_ctx=%p loaded certificate '%s' with key '%s'", ctx, path, key); + } + if (certificates_count && !*cfg.certificates) + { + ERR("ssl_ctx=%p none of the required certificates were loaded!", ctx); + return EINVAL; + } + + if (private_keys_count && !*cfg.private_keys) + { + ERR("ssl_ctx=%p none of the required private keys were loaded!", ctx); + return EINVAL; + } + else if (pk_node != eina_list_last(*cfg.private_keys)) + { + do + { + n = pk_node->next; + path = n->data; + ERR("ssl_ctx=%p extra private key is unused '%s'", ctx, path); + eina_stringshare_del(path); + *cfg.private_keys = eina_list_remove_list(*cfg.private_keys, n); + } + while (pk_node->next); + } + + EINA_LIST_FOREACH_SAFE(*cfg.certificate_revogation_lists, n, n_next, path) + { + r = gnutls_certificate_set_x509_crl_file(ctx->x509_cred, path, GNUTLS_X509_FMT_PEM); + if (r < 0) + { + ERR("ssl_ctx=%p could not use certificate revogation lists from %s: %s", + ctx, path, gnutls_strerror(r)); + eina_stringshare_del(path); + *cfg.certificate_revogation_lists = eina_list_remove_list(*cfg.certificate_revogation_lists, n); + continue; + } + + DBG("ssl_ctx=%p loaded certificate revogation lists '%s'", ctx, path); + } + if (certificate_revogation_lists_count && !*cfg.certificate_revogation_lists) + { + ERR("ssl_ctx=%p none of the required certificate revogation lists were loaded!", ctx); + return EINVAL; + } + + EINA_LIST_FOREACH_SAFE(*cfg.certificate_authorities, n, n_next, path) + { + struct stat st; + + r = 0; + if (stat(path, &st) != 0) + { + ERR("ssl_ctx=%p could not load certificate authorities from '%s': %s", ctx, path, strerror(errno)); + eina_stringshare_del(path); + *cfg.certificate_authorities = eina_list_remove_list(*cfg.certificate_authorities, n); + continue; + } + else if (S_ISDIR(st.st_mode)) + r = gnutls_certificate_set_x509_trust_dir(ctx->x509_cred, path, GNUTLS_X509_FMT_PEM); + else + r = gnutls_certificate_set_x509_trust_file(ctx->x509_cred, path, GNUTLS_X509_FMT_PEM); + + if (r < 0) + { + ERR("ssl_ctx=%p could not use certificate authorities from '%s': %s", ctx, path, gnutls_strerror(r)); + eina_stringshare_del(path); + *cfg.certificate_authorities = eina_list_remove_list(*cfg.certificate_authorities, n); + continue; + } + + DBG("ssl_ctx=%p loaded certificate authorities '%s'", ctx, path); + } + if (certificate_authorities_count && !*cfg.certificate_authorities) + { + ERR("ssl_ctx=%p none of the required certificate authorities were loaded!", ctx); + return EINVAL; + } + + return 0; +} + +static void * +efl_net_ssl_ctx_connection_new(Efl_Net_Ssl_Ctx *ctx) +{ + gnutls_session_t session; + int r; + + r = gnutls_init(&session, ctx->is_dialer ? GNUTLS_CLIENT : GNUTLS_SERVER); + if (r < 0) + { + ERR("ssl_ctx=%p could not create %s session: %s", + ctx, ctx->is_dialer ? "dialer" : "server", gnutls_strerror(r)); + return NULL; + } + + if (!ctx->priority) + { + r = gnutls_set_default_priority(session); + if (r < 0) + { + ERR("ssl_ctx=%p could not set default cipher priority: %s", ctx, gnutls_strerror(r)); + goto error; + } + } + else + { + r = gnutls_priority_set(session, ctx->priority); + if (r < 0) + { + ERR("ssl_ctx=%p could not set cipher priority: %s", ctx, gnutls_strerror(r)); + goto error; + } + } + + r = gnutls_credentials_set(session, GNUTLS_CRD_CERTIFICATE, ctx->x509_cred); + if (r < 0) + { + ERR("ssl_ctx=%p could not set session credentials: %s", ctx, gnutls_strerror(r)); + goto error; + } + + return session; + + error: + gnutls_deinit(session); + return NULL; +} + +static Eina_Error +efl_net_ssl_ctx_setup(Efl_Net_Ssl_Ctx *ctx, Efl_Net_Ssl_Ctx_Config cfg) +{ + Eina_Error err; + const char *priority; + int r; + + EINA_SAFETY_ON_TRUE_RETURN_VAL(ctx->x509_cred != NULL, EALREADY); + + switch (cfg.cipher) + { + case EFL_NET_SSL_CIPHER_AUTO: + priority = NULL; + break; + case EFL_NET_SSL_CIPHER_SSLV3: + priority = "NORMAL:%VERIFY_ALLOW_X509_V1_CA_CRT:!VERS-TLS1.0:!VERS-TLS1.1:!VERS-TLS1.2"; + break; + case EFL_NET_SSL_CIPHER_TLSV1: + priority = "NORMAL:%VERIFY_ALLOW_X509_V1_CA_CRT:!VERS-SSL3.0!VERS-TLS1.1:!VERS-TLS1.2"; + break; + case EFL_NET_SSL_CIPHER_TLSV1_1: + priority = "NORMAL:%VERIFY_ALLOW_X509_V1_CA_CRT:!VERS-SSL3.0:!VERS-TLS1.0:!VERS-TLS1.2"; + break; + case EFL_NET_SSL_CIPHER_TLSV1_2: + priority = "NORMAL:%VERIFY_ALLOW_X509_V1_CA_CRT:!VERS-SSL3.0:!VERS-TLS1.0:!VERS-TLS1.1"; + break; + default: + ERR("ssl_ctx=%p unsupported cipher %d", ctx, cfg.cipher); + return EINVAL; + } + + if (priority) + { + const char *err_pos = NULL; + r = gnutls_priority_init(&ctx->priority, priority, &err_pos); + if (r < 0) + { + size_t off = err_pos - priority; + if (r == GNUTLS_E_INVALID_REQUEST) + { + ERR("ssl_ctx=%p invalid syntax on GNUTLS priority string offset %zd: '%s'", ctx, off, priority); + return EINVAL; + } + ERR("ssl_ctx=%p could not set GNUTLS priority offset %zd '%s': %s", ctx, off, priority, gnutls_strerror(r)); + return EINVAL; + } + } + + r = gnutls_certificate_allocate_credentials(&ctx->x509_cred); + if (r < 0) + { + ERR("ssl_ctx=%p could not allocate X509 credentials: %s", ctx, gnutls_strerror(r)); + err = ENOSYS; + goto err_cert_alloc; + } + + err = _efl_net_ssl_ctx_load_lists(ctx, cfg); + if (err) + { + ERR("ssl_ctx=%p failed to load certificate, private keys, CRL or CA", ctx); + goto err_load; + } + + return 0; + + err_load: + gnutls_certificate_free_credentials(ctx->x509_cred); + ctx->x509_cred = NULL; + err_cert_alloc: + gnutls_priority_deinit(ctx->priority); + ctx->priority = NULL; + return err; +} + +static void +efl_net_ssl_ctx_teardown(Efl_Net_Ssl_Ctx *ctx) +{ + if (ctx->x509_cred) + { + gnutls_certificate_free_credentials(ctx->x509_cred); + ctx->x509_cred = NULL; + } + + if (ctx->priority) + { + gnutls_priority_deinit(ctx->priority); + ctx->priority = NULL; + } +} + +static Eina_Error +efl_net_ssl_ctx_verify_mode_set(Efl_Net_Ssl_Ctx *ctx EINA_UNUSED, Efl_Net_Ssl_Verify_Mode verify_mode EINA_UNUSED) +{ + return 0; +} + +static Eina_Error +efl_net_ssl_ctx_hostname_verify_set(Efl_Net_Ssl_Ctx *ctx EINA_UNUSED, Eina_Bool hostname_verify EINA_UNUSED) +{ + return 0; +} + +static Eina_Error +efl_net_ssl_ctx_hostname_set(Efl_Net_Ssl_Ctx *ctx EINA_UNUSED, const char *hostname EINA_UNUSED) +{ + return 0; +} diff --git a/src/lib/ecore_con/efl_net_ssl_ctx-none.c b/src/lib/ecore_con/efl_net_ssl_ctx-none.c new file mode 100644 index 0000000..1332a0f --- /dev/null +++ b/src/lib/ecore_con/efl_net_ssl_ctx-none.c @@ -0,0 +1,38 @@ +struct _Efl_Net_Ssl_Ctx { +}; + +static void * +efl_net_ssl_ctx_connection_new(Efl_Net_Ssl_Ctx *ctx EINA_UNUSED) +{ + return NULL; +} + +static Eina_Error +efl_net_ssl_ctx_setup(Efl_Net_Ssl_Ctx *ctx EINA_UNUSED, Efl_Net_Ssl_Ctx_Config cfg EINA_UNUSED) +{ + ERR("EFL compiled with --with-crypto=none"); + return ENOSYS; +} + +static void +efl_net_ssl_ctx_teardown(Efl_Net_Ssl_Ctx *ctx EINA_UNUSED) +{ +} + +static Eina_Error +efl_net_ssl_ctx_verify_mode_set(Efl_Net_Ssl_Ctx *ctx EINA_UNUSED, Efl_Net_Ssl_Verify_Mode verify_mode EINA_UNUSED) +{ + return ENOSYS; +} + +static Eina_Error +efl_net_ssl_ctx_hostname_verify_set(Efl_Net_Ssl_Ctx *ctx EINA_UNUSED, Eina_Bool hostname_verify EINA_UNUSED) +{ + return ENOSYS; +} + +static Eina_Error +efl_net_ssl_ctx_hostname_set(Efl_Net_Ssl_Ctx *ctx EINA_UNUSED, const char *hostname EINA_UNUSED) +{ + return ENOSYS; +} diff --git a/src/lib/ecore_con/efl_net_ssl_ctx-openssl.c b/src/lib/ecore_con/efl_net_ssl_ctx-openssl.c new file mode 100644 index 0000000..beefbac --- /dev/null +++ b/src/lib/ecore_con/efl_net_ssl_ctx-openssl.c @@ -0,0 +1,387 @@ +#include <openssl/ssl.h> +#include <openssl/err.h> +#include <openssl/dh.h> + +struct _Efl_Net_Ssl_Ctx +{ + SSL_CTX *ssl_ctx; + Eina_Bool did_certificates; + Eina_Bool is_dialer; +}; + +#define EFL_NET_SSL_CONTEXT_CIPHERS "aRSA+HIGH:+kEDH:+kRSA:!kSRP:!kPSK:+3DES:!MD5" + +#define _efl_net_ssl_ctx_check_errors() \ + __efl_net_ssl_ctx_check_errors(__FILE__, __LINE__, __FUNCTION__) +static unsigned long +__efl_net_ssl_ctx_check_errors(const char *file, int line, const char *fname) +{ + unsigned long first = 0; + do + { + const char *_ssl_err_file, *_ssl_err_data; + int _ssl_err_line, _ssl_err_flags; + unsigned long _ssl_err = ERR_get_error_line_data(&_ssl_err_file, &_ssl_err_line, &_ssl_err_data, &_ssl_err_flags); + if (!_ssl_err) break; + if (!first) first = _ssl_err; + eina_log_print(_ecore_con_log_dom, EINA_LOG_LEVEL_ERR, file, fname, line, + "OpenSSL error %s:%d%s%s: %s", + _ssl_err_file, _ssl_err_line, + (_ssl_err_flags & ERR_TXT_STRING) ? " " : "", + (_ssl_err_flags & ERR_TXT_STRING) ? _ssl_err_data : "", + ERR_reason_error_string(_ssl_err)); + } + while (1); + return first; +} + +static Eina_Error +_efl_net_ssl_ctx_load_lists(Efl_Net_Ssl_Ctx *ctx, Efl_Net_Ssl_Ctx_Config cfg) +{ + Eina_List *n, *n_next; + const char *path; + unsigned certificates_count = eina_list_count(*cfg.certificates); + unsigned private_keys_count = eina_list_count(*cfg.private_keys); + unsigned certificate_revogation_lists_count = eina_list_count(*cfg.certificate_revogation_lists); + unsigned certificate_authorities_count = eina_list_count(*cfg.certificate_authorities); + long err_ssl; + const char *err_file; + const char *err_data; + int err_line, err_flags; + X509_STORE *x509_store; + X509_LOOKUP *x509_lookup; + unsigned long x509_store_flags = X509_V_FLAG_TRUSTED_FIRST; + + if (cfg.load_defaults) + { + if (SSL_CTX_set_default_verify_paths(ctx->ssl_ctx) != 1) + { + _efl_net_ssl_ctx_check_errors(); + ERR("ssl_ctx=%p could not load default paths", ctx); + return ENOSYS; + } + DBG("ssl_ctx=%p loaded default paths", ctx); + } + else + DBG("ssl_ctx=%p did not load default paths", ctx); + + EINA_LIST_FOREACH_SAFE(*cfg.certificates, n, n_next, path) + { + if ((SSL_CTX_use_certificate_file(ctx->ssl_ctx, path, SSL_FILETYPE_PEM) != 1) && + (SSL_CTX_use_certificate_file(ctx->ssl_ctx, path, SSL_FILETYPE_ASN1) != 1)) + { + err_ssl = ERR_peek_error_line_data(&err_file, &err_line, &err_data, &err_flags); + +_efl_net_ssl_ctx_check_errors(); + _efl_net_ssl_ctx_check_errors(); + + ERR("ssl_ctx=%p could not use certificate from %s [%s:%d%s%s '%s']", + ctx, path, + err_file, err_line, + (err_flags & ERR_TXT_STRING) ? " " : "", + (err_flags & ERR_TXT_STRING) ? err_data : "", + ERR_reason_error_string(err_ssl)); + eina_stringshare_del(path); + *cfg.certificates = eina_list_remove_list(*cfg.certificates, n); + continue; + } + + DBG("ssl_ctx=%p loaded certificate '%s'", ctx, path); + ctx->did_certificates = EINA_TRUE; + } + if (certificates_count && !*cfg.certificates) + { + ERR("ssl_ctx=%p none of the required certificates were loaded!", ctx); + return EINVAL; + } + + EINA_LIST_FOREACH_SAFE(*cfg.private_keys, n, n_next, path) + { + if ((SSL_CTX_use_PrivateKey_file(ctx->ssl_ctx, path, SSL_FILETYPE_PEM) != 1) && + (SSL_CTX_use_PrivateKey_file(ctx->ssl_ctx, path, SSL_FILETYPE_ASN1) != 1)) + { + err_ssl = ERR_peek_error_line_data(&err_file, &err_line, &err_data, &err_flags); + _efl_net_ssl_ctx_check_errors(); + + ERR("ssl_ctx=%p could not use private key from %s [%s:%d%s%s '%s']", + ctx, path, + err_file, err_line, + (err_flags & ERR_TXT_STRING) ? " " : "", + (err_flags & ERR_TXT_STRING) ? err_data : "", + ERR_reason_error_string(err_ssl)); + eina_stringshare_del(path); + *cfg.private_keys = eina_list_remove_list(*cfg.private_keys, n); + continue; + } + + if (SSL_CTX_check_private_key(ctx->ssl_ctx) != 1) + { + err_ssl = ERR_peek_error_line_data(&err_file, &err_line, &err_data, &err_flags); + _efl_net_ssl_ctx_check_errors(); + + ERR("ssl_ctx=%p could not check private key from %s [%s:%d%s%s '%s']", + ctx, path, + err_file, err_line, + (err_flags & ERR_TXT_STRING) ? " " : "", + (err_flags & ERR_TXT_STRING) ? err_data : "", + ERR_reason_error_string(err_ssl)); + continue; + } + + DBG("ssl_ctx=%p loaded private key '%s'", ctx, path); + } + if (private_keys_count && !*cfg.private_keys) + { + ERR("ssl_ctx=%p none of the required private keys were loaded!", ctx); + return EINVAL; + } + + x509_store = SSL_CTX_get_cert_store(ctx->ssl_ctx); + if (!x509_store) + { + _efl_net_ssl_ctx_check_errors(); + ERR("ssl_ctx=%p SSL has no X509 certificate store", ctx); + return ENOSYS; + } + x509_lookup = X509_STORE_add_lookup(x509_store, X509_LOOKUP_file()); + if (!x509_lookup) + { + _efl_net_ssl_ctx_check_errors(); + ERR("ssl_ctx=%p could not add X509 file lookup", ctx); + return ENOSYS; + } + + EINA_LIST_FOREACH_SAFE(*cfg.certificate_revogation_lists, n, n_next, path) + { + if ((X509_load_crl_file(x509_lookup, path, X509_FILETYPE_PEM) != 1) && + (X509_load_crl_file(x509_lookup, path, X509_FILETYPE_ASN1) != 1)) + { + err_ssl = ERR_peek_error_line_data(&err_file, &err_line, &err_data, &err_flags); + _efl_net_ssl_ctx_check_errors(); + + ERR("ssl_ctx=%p could not use certificate revogation lists from %s [%s:%d%s%s '%s']", + ctx, path, + err_file, err_line, + (err_flags & ERR_TXT_STRING) ? " " : "", + (err_flags & ERR_TXT_STRING) ? err_data : "", + ERR_reason_error_string(err_ssl)); + eina_stringshare_del(path); + *cfg.certificate_revogation_lists = eina_list_remove_list(*cfg.certificate_revogation_lists, n); + continue; + } + + DBG("ssl_ctx=%p loaded certificate revogation lists '%s'", ctx, path); + x509_store_flags |= X509_V_FLAG_CRL_CHECK | X509_V_FLAG_CRL_CHECK_ALL; + } + if (certificate_revogation_lists_count && !*cfg.certificate_revogation_lists) + { + ERR("ssl_ctx=%p none of the required certificate revogation lists were loaded!", ctx); + return EINVAL; + } + X509_STORE_set_flags(x509_store, x509_store_flags); + + EINA_LIST_FOREACH_SAFE(*cfg.certificate_authorities, n, n_next, path) + { + struct stat st; + const char *cafile = NULL, *cadir = NULL; + + if (stat(path, &st) != 0) + { + ERR("ssl_ctx=%p could not load certificate authorities from '%s': %s", ctx, path, strerror(errno)); + eina_stringshare_del(path); + *cfg.certificate_authorities = eina_list_remove_list(*cfg.certificate_authorities, n); + continue; + } + else if (S_ISDIR(st.st_mode)) cadir = path; + else cafile = path; + + if (SSL_CTX_load_verify_locations(ctx->ssl_ctx, cafile, cadir) != 1) + { + err_ssl = ERR_peek_error_line_data(&err_file, &err_line, &err_data, &err_flags); + _efl_net_ssl_ctx_check_errors(); + + ERR("ssl_ctx=%p could not use certificate authorities from %s [%s:%d%s%s '%s']", + ctx, path, + err_file, err_line, + (err_flags & ERR_TXT_STRING) ? " " : "", + (err_flags & ERR_TXT_STRING) ? err_data : "", + ERR_reason_error_string(err_ssl)); + eina_stringshare_del(path); + *cfg.certificate_authorities = eina_list_remove_list(*cfg.certificate_authorities, n); + continue; + } + + DBG("ssl_ctx=%p loaded certificate authorities '%s'", ctx, path); + } + if (certificate_authorities_count && !*cfg.certificate_authorities) + { + ERR("ssl_ctx=%p none of the required certificate authorities were loaded!", ctx); + return EINVAL; + } + + if (!ctx->did_certificates) + { + if (!SSL_CTX_set_cipher_list(ctx->ssl_ctx, EFL_NET_SSL_CONTEXT_CIPHERS)) + { + err_ssl = ERR_peek_error_line_data(&err_file, &err_line, &err_data, &err_flags); + _efl_net_ssl_ctx_check_errors(); + + ERR("ssl_ctx=%p Could not set ciphers '%s' [%s:%d%s%s '%s']", + ctx, EFL_NET_SSL_CONTEXT_CIPHERS, + err_file, err_line, + (err_flags & ERR_TXT_STRING) ? " " : "", + (err_flags & ERR_TXT_STRING) ? err_data : "", + ERR_reason_error_string(err_ssl)); + return EINVAL; + } + } + + return 0; +} + +static void * +efl_net_ssl_ctx_connection_new(Efl_Net_Ssl_Ctx *ctx) +{ + return SSL_new(ctx->ssl_ctx); +} + +static Eina_Error +efl_net_ssl_ctx_setup(Efl_Net_Ssl_Ctx *ctx, Efl_Net_Ssl_Ctx_Config cfg) +{ + Eina_Error err; + unsigned long options; + + EINA_SAFETY_ON_TRUE_RETURN_VAL(ctx->ssl_ctx != NULL, EALREADY); + + ctx->is_dialer = cfg.is_dialer; + if (ctx->is_dialer) + { + switch (cfg.cipher) + { + case EFL_NET_SSL_CIPHER_AUTO: + ctx->ssl_ctx = SSL_CTX_new(SSLv23_client_method()); + break; + case EFL_NET_SSL_CIPHER_SSLV3: +#ifndef OPENSSL_NO_SSL3_METHOD + ctx->ssl_ctx = SSL_CTX_new(SSLv3_client_method()); +#else + ERR("ssl_ctx=%p SSLv3 is disabled in your OpenSSL build", ctx); +#endif + break; + case EFL_NET_SSL_CIPHER_TLSV1: + ctx->ssl_ctx = SSL_CTX_new(TLSv1_client_method()); + break; + case EFL_NET_SSL_CIPHER_TLSV1_1: + ctx->ssl_ctx = SSL_CTX_new(TLSv1_1_client_method()); + break; + case EFL_NET_SSL_CIPHER_TLSV1_2: + ctx->ssl_ctx = SSL_CTX_new(TLSv1_2_client_method()); + break; + default: + ERR("ssl_ctx=%p unsupported cipher %d", ctx, cfg.cipher); + return EINVAL; + } + + EINA_SAFETY_ON_NULL_RETURN_VAL(ctx->ssl_ctx, ENOSYS); + } + else + { + switch (cfg.cipher) + { + case EFL_NET_SSL_CIPHER_AUTO: + ctx->ssl_ctx = SSL_CTX_new(SSLv23_server_method()); + break; + case EFL_NET_SSL_CIPHER_SSLV3: +#ifndef OPENSSL_NO_SSL3_METHOD + ctx->ssl_ctx = SSL_CTX_new(SSLv3_server_method()); +#else + ERR("ssl_ctx=%p SSLv3 is disabled in your OpenSSL build", ctx); +#endif + break; + case EFL_NET_SSL_CIPHER_TLSV1: + ctx->ssl_ctx = SSL_CTX_new(TLSv1_server_method()); + break; + case EFL_NET_SSL_CIPHER_TLSV1_1: + ctx->ssl_ctx = SSL_CTX_new(TLSv1_1_server_method()); + break; + case EFL_NET_SSL_CIPHER_TLSV1_2: + ctx->ssl_ctx = SSL_CTX_new(TLSv1_2_server_method()); + break; + default: + ERR("ssl_ctx=%p unsupported cipher %d", ctx, cfg.cipher); + return EINVAL; + } + + EINA_SAFETY_ON_NULL_RETURN_VAL(ctx->ssl_ctx, ENOSYS); + } + + options = SSL_CTX_get_options(ctx->ssl_ctx); + options |= SSL_OP_NO_SSLv2; + options |= SSL_OP_SINGLE_DH_USE; + + if (cfg.cipher != EFL_NET_SSL_CIPHER_SSLV3) + options |= SSL_OP_NO_SSLv3; + + SSL_CTX_set_options(ctx->ssl_ctx, options); + + err = _efl_net_ssl_ctx_load_lists(ctx, cfg); + if (err) + { + ERR("ssl_ctx=%p failed to load certificate, private keys, CRL or CA", ctx); + goto error; + } + + return 0; + + error: + SSL_CTX_free(ctx->ssl_ctx); + ctx->ssl_ctx = NULL; + return err; +} + +static void +efl_net_ssl_ctx_teardown(Efl_Net_Ssl_Ctx *ctx) +{ + if (ctx->ssl_ctx) + { + SSL_CTX_free(ctx->ssl_ctx); + ctx->ssl_ctx = NULL; + } +} + +static Eina_Error +efl_net_ssl_ctx_verify_mode_set(Efl_Net_Ssl_Ctx *ctx, Efl_Net_Ssl_Verify_Mode verify_mode) +{ + int ssl_mode; + + switch (verify_mode) + { + case EFL_NET_SSL_VERIFY_MODE_NONE: + ssl_mode = SSL_VERIFY_NONE; + break; + case EFL_NET_SSL_VERIFY_MODE_OPTIONAL: + ssl_mode = SSL_VERIFY_PEER; + break; + case EFL_NET_SSL_VERIFY_MODE_REQUIRED: + ssl_mode = SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT; + break; + default: + ERR("unknown verify_mode=%d", verify_mode); + return EINVAL; + } + + SSL_CTX_set_verify(ctx->ssl_ctx, ssl_mode, SSL_CTX_get_verify_callback(ctx->ssl_ctx)); + return 0; +} + +static Eina_Error +efl_net_ssl_ctx_hostname_verify_set(Efl_Net_Ssl_Ctx *ctx EINA_UNUSED, Eina_Bool hostname_verify EINA_UNUSED) +{ + return 0; +} + +static Eina_Error +efl_net_ssl_ctx_hostname_set(Efl_Net_Ssl_Ctx *ctx EINA_UNUSED, const char *hostname EINA_UNUSED) +{ + return 0; +} diff --git a/src/lib/ecore_con/efl_net_ssl_types.eot b/src/lib/ecore_con/efl_net_ssl_types.eot new file mode 100644 index 0000000..6556f7c --- /dev/null +++ b/src/lib/ecore_con/efl_net_ssl_types.eot @@ -0,0 +1,26 @@ +enum Efl.Net.Ssl.Verify_Mode { + [[Defines how remote peers should be verified. + + @since 1.19 + ]] + none, [[Do not verify peer]] + optional, [[If provided, verify. Otherwise proceed]] + required, [[Always verify and fail if certificate wasn't provided]] +} + +enum Efl.Net.Ssl.Cipher { + [[Defines the SSL/TLS version to use. + + Prefer 'auto' or one of the TLS variants. + + \@note since it's very insecure, SSLv2 is not present. SSLv3 + support depends on being available on the platform. + + @since 1.19 + ]] + auto, [[The default. Use the best your system supports, disables dangerous ciphers]] + sslv3, [[SSLv3, insecure and unsupported - DANGEROUS]] + tlsv1, [[TLSv1, secure and widely available]] + tlsv1_1, [[TLSv1.1, secure]] + tlsv1_2, [[TLSv1.2, secure]] +} --
