/***************************************************************************** * gnutls.c ***************************************************************************** * Copyright (C) 2004-2017 RĂ©mi Denis-Courmont * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 2.1 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA. *****************************************************************************/ #ifdef HAVE_CONFIG_H # include "config.h" #endif #include #include #include #include #include #include #include #ifdef HAVE_SYS_UIO_H # include #endif #include #include #include #include #include #include #include typedef struct vlc_tls_gnutls { vlc_tls_t tls; gnutls_session_t session; vlc_object_t *obj; } vlc_tls_gnutls_t; static int gnutls_Init (vlc_object_t *obj) { const char *version = gnutls_check_version ("3.3.0"); if (version == NULL) { msg_Err (obj, "unsupported GnuTLS version"); return -1; } msg_Dbg (obj, "using GnuTLS version %s", version); return 0; } static int gnutls_Error(vlc_tls_gnutls_t *priv, int val) { switch (val) { case GNUTLS_E_AGAIN: #ifdef _WIN32 WSASetLastError (WSAEWOULDBLOCK); #endif errno = EAGAIN; break; case GNUTLS_E_INTERRUPTED: #ifdef _WIN32 WSASetLastError (WSAEINTR); #endif errno = EINTR; break; default: msg_Err(priv->obj, "%s", gnutls_strerror (val)); #ifndef NDEBUG if (!gnutls_error_is_fatal (val)) msg_Err(priv->obj, "Error above should be handled"); #endif #ifdef _WIN32 WSASetLastError (WSAECONNRESET); #endif errno = ECONNRESET; } return -1; } static ssize_t vlc_gnutls_read(gnutls_transport_ptr_t ptr, void *buf, size_t length) { vlc_tls_t *sock = ptr; struct iovec iov = { .iov_base = buf, .iov_len = length, }; return sock->readv(sock, &iov, 1); } static ssize_t vlc_gnutls_writev(gnutls_transport_ptr_t ptr, const giovec_t *giov, int iovcnt) { #ifdef IOV_MAX const long iovmax = IOV_MAX; #else const long iovmax = sysconf(_SC_IOV_MAX); #endif if (unlikely(iovcnt > iovmax)) { errno = EINVAL; return -1; } if (unlikely(iovcnt == 0)) return 0; vlc_tls_t *sock = ptr; struct iovec iov[iovcnt]; for (int i = 0; i < iovcnt; i++) { iov[i].iov_base = giov[i].iov_base; iov[i].iov_len = giov[i].iov_len; } return sock->writev(sock, iov, iovcnt); } static int gnutls_GetFD(vlc_tls_t *tls) { vlc_tls_gnutls_t *priv = (vlc_tls_gnutls_t *)tls; vlc_tls_t *sock = gnutls_transport_get_ptr(priv->session); return vlc_tls_GetFD(sock); } static ssize_t gnutls_Recv(vlc_tls_t *tls, struct iovec *iov, unsigned count) { vlc_tls_gnutls_t *priv = (vlc_tls_gnutls_t *)tls; gnutls_session_t session = priv->session; size_t rcvd = 0; while (count > 0) { ssize_t val = gnutls_record_recv(session, iov->iov_base, iov->iov_len); if (val < 0) return rcvd ? (ssize_t)rcvd : gnutls_Error(priv, val); rcvd += val; if ((size_t)val < iov->iov_len) break; iov++; count--; } return rcvd; } static ssize_t gnutls_Send (vlc_tls_t *tls, const struct iovec *iov, unsigned count) { vlc_tls_gnutls_t *priv = (vlc_tls_gnutls_t *)tls; gnutls_session_t session = priv->session; ssize_t val; if (!gnutls_record_check_corked(session)) { gnutls_record_cork(session); while (count > 0) { val = gnutls_record_send(session, iov->iov_base, iov->iov_len); if (val < (ssize_t)iov->iov_len) break; iov++; count--; } } val = gnutls_record_uncork(session, 0); return (val < 0) ? gnutls_Error(priv, val) : val; } static int gnutls_Shutdown(vlc_tls_t *tls, bool duplex) { vlc_tls_gnutls_t *priv = (vlc_tls_gnutls_t *)tls; gnutls_session_t session = priv->session; ssize_t val; /* Flush any pending data */ val = gnutls_record_uncork(session, 0); if (val < 0) return gnutls_Error(priv, val); val = gnutls_bye(session, duplex ? GNUTLS_SHUT_RDWR : GNUTLS_SHUT_WR); if (val < 0) return gnutls_Error(priv, val); return 0; } static void gnutls_Close (vlc_tls_t *tls) { vlc_tls_gnutls_t *priv = (vlc_tls_gnutls_t *)tls; gnutls_deinit(priv->session); free(priv); } static vlc_tls_gnutls_t *gnutls_SessionOpen(vlc_tls_creds_t *creds, int type, gnutls_certificate_credentials_t x509, vlc_tls_t *sock, const char *const *alpn) { vlc_tls_gnutls_t *priv = malloc(sizeof (*priv)); if (unlikely(priv == NULL)) return NULL; gnutls_session_t session; const char *errp; int val; type |= GNUTLS_NONBLOCK; #if (GNUTLS_VERSION_NUMBER >= 0x030500) type |= GNUTLS_ENABLE_FALSE_START; #endif val = gnutls_init(&session, type); if (val != 0) { msg_Err(creds, "cannot initialize TLS session: %s", gnutls_strerror(val)); free(priv); return NULL; } char *priorities = var_InheritString(creds, "gnutls-priorities"); if (unlikely(priorities == NULL)) goto error; val = gnutls_priority_set_direct (session, priorities, &errp); if (val < 0) msg_Err(creds, "cannot set TLS priorities \"%s\": %s", errp, gnutls_strerror(val)); free (priorities); if (val < 0) goto error; val = gnutls_credentials_set (session, GNUTLS_CRD_CERTIFICATE, x509); if (val < 0) { msg_Err(creds, "cannot set TLS session credentials: %s", gnutls_strerror(val)); goto error; } if (alpn != NULL) { gnutls_datum_t *protv = NULL; unsigned protc = 0; while (*alpn != NULL) { gnutls_datum_t *n = realloc(protv, sizeof (*protv) * (protc + 1)); if (unlikely(n == NULL)) { free(protv); goto error; } protv = n; protv[protc].data = (void *)*alpn; protv[protc].size = strlen(*alpn); protc++; alpn++; } val = gnutls_alpn_set_protocols (session, protv, protc, 0); free (protv); } gnutls_transport_set_ptr(session, sock); gnutls_transport_set_vec_push_function(session, vlc_gnutls_writev); gnutls_transport_set_pull_function(session, vlc_gnutls_read); priv->session = session; priv->obj = VLC_OBJECT(creds); vlc_tls_t *tls = &priv->tls; tls->get_fd = gnutls_GetFD; tls->readv = gnutls_Recv; tls->writev = gnutls_Send; tls->shutdown = gnutls_Shutdown; tls->close = gnutls_Close; return priv; error: gnutls_deinit (session); free(priv); return NULL; } /** * Starts or continues the TLS handshake. * * @return -1 on fatal error, 0 on successful handshake completion, * 1 if more would-be blocking recv is needed, * 2 if more would-be blocking send is required. */ static int gnutls_ContinueHandshake(vlc_tls_creds_t *crd, vlc_tls_gnutls_t *priv, char **restrict alp) { gnutls_session_t session = priv->session; int val; #ifdef _WIN32 WSASetLastError (0); #endif do { val = gnutls_handshake (session); msg_Dbg(crd, "TLS handshake: %s", gnutls_strerror (val)); switch (val) { case GNUTLS_E_SUCCESS: goto done; case GNUTLS_E_AGAIN: case GNUTLS_E_INTERRUPTED: /* I/O event: return to caller's poll() loop */ return 1 + gnutls_record_get_direction (session); } } while (!gnutls_error_is_fatal (val)); #ifdef _WIN32 msg_Dbg(crd, "Winsock error %d", WSAGetLastError ()); #endif msg_Err(crd, "TLS handshake error: %s", gnutls_strerror (val)); return -1; done: #if (GNUTLS_VERSION_NUMBER >= 0x030500) /* intentionally left blank */; unsigned flags = gnutls_session_get_flags(session); if (flags & GNUTLS_SFLAGS_SAFE_RENEGOTIATION) msg_Dbg(crd, " - safe renegotiation (RFC5746) enabled"); if (flags & GNUTLS_SFLAGS_EXT_MASTER_SECRET) msg_Dbg(crd, " - extended master secret (RFC7627) enabled"); if (flags & GNUTLS_SFLAGS_ETM) msg_Dbg(crd, " - encrypt then MAC (RFC7366) enabled"); if (flags & GNUTLS_SFLAGS_FALSE_START) msg_Dbg(crd, " - false start (RFC7918) enabled"); #endif if (alp != NULL) { gnutls_datum_t datum; val = gnutls_alpn_get_selected_protocol (session, &datum); if (val == 0) { if (memchr (datum.data, 0, datum.size) != NULL) return -1; /* Other end is doing something fishy?! */ *alp = strndup ((char *)datum.data, datum.size); if (unlikely(*alp == NULL)) return -1; } else *alp = NULL; } return 0; } static vlc_tls_t *gnutls_ClientSessionOpen(vlc_tls_creds_t *crd, vlc_tls_t *sk, const char *hostname, const char *const *alpn) { vlc_tls_gnutls_t *priv; priv = gnutls_SessionOpen(crd, GNUTLS_CLIENT, crd->sys, sk, alpn); if (priv == NULL) return NULL; gnutls_session_t session = priv->session; /* minimum DH prime bits */ gnutls_dh_set_prime_bits (session, 1024); if (likely(hostname != NULL)) /* fill Server Name Indication */ gnutls_server_name_set (session, GNUTLS_NAME_DNS, hostname, strlen (hostname)); return &priv->tls; } static int gnutls_ClientHandshake(vlc_tls_creds_t *creds, vlc_tls_t *tls, const char *host, const char *service, char **restrict alp) { vlc_tls_gnutls_t *priv = (vlc_tls_gnutls_t *)tls; int val = gnutls_ContinueHandshake(creds, priv, alp); if (val) return val; /* certificates chain verification */ gnutls_session_t session = priv->session; unsigned status; val = gnutls_certificate_verify_peers3 (session, host, &status); if (val) { msg_Err(creds, "Certificate verification error: %s", gnutls_strerror(val)); goto error; } if (status == 0) /* Good certificate */ return 0; /* Bad certificate */ gnutls_datum_t desc; if (gnutls_certificate_verification_status_print(status, gnutls_certificate_type_get (session), &desc, 0) == 0) { msg_Err(creds, "Certificate verification failure: %s", desc.data); gnutls_free (desc.data); } const unsigned status_insecure = GNUTLS_CERT_SIGNER_NOT_FOUND | GNUTLS_CERT_INVALID; if ((status & status_insecure) == status_insecure && (creds->obj.flags & OBJECT_FLAGS_INSECURE)) { msg_Info( creds, "Accepting self-signed/untrusted CA certificate." ); return 0; } status &= ~GNUTLS_CERT_INVALID; /* always set / catch-all error */ status &= ~GNUTLS_CERT_SIGNER_NOT_FOUND; /* unknown CA */ status &= ~GNUTLS_CERT_UNEXPECTED_OWNER; /* mismatched hostname */ if (status != 0 || host == NULL) goto error; /* Really bad certificate */ /* Look up mismatching certificate in store */ const gnutls_datum_t *datum; unsigned count; datum = gnutls_certificate_get_peers (session, &count); if (datum == NULL || count == 0) { msg_Err(creds, "Peer certificate not available"); goto error; } msg_Dbg(creds, "%u certificate(s) in the list", count); val = gnutls_verify_stored_pubkey (NULL, NULL, host, service, GNUTLS_CRT_X509, datum, 0); const char *msg; switch (val) { case 0: msg_Dbg(creds, "certificate key match for %s", host); return 0; case GNUTLS_E_NO_CERTIFICATE_FOUND: msg_Dbg(creds, "no known certificates for %s", host); msg = N_("However, the security certificate presented by the " "server is unknown and could not be authenticated by any " "trusted Certificate Authority."); break; case GNUTLS_E_CERTIFICATE_KEY_MISMATCH: msg_Dbg(creds, "certificate keys mismatch for %s", host); msg = N_("However, the security certificate presented by the " "server changed since the previous visit and was not " "authenticated by any trusted Certificate Authority."); break; default: msg_Err(creds, "certificate key match error for %s: %s", host, gnutls_strerror(val)); goto error; } if (vlc_dialog_wait_question(creds, VLC_DIALOG_QUESTION_WARNING, _("Abort"), _("View certificate"), NULL, _("Insecure site"), _("You attempted to reach %s. %s\n" "This problem may be stem from an attempt to breach your security, " "compromise your privacy, or a configuration error.\n\n" "If in doubt, abort now.\n"), host, vlc_gettext(msg)) != 1) goto error; gnutls_x509_crt_t cert; if (gnutls_x509_crt_init (&cert)) goto error; if (gnutls_x509_crt_import (cert, datum, GNUTLS_X509_FMT_DER) || gnutls_x509_crt_print (cert, GNUTLS_CRT_PRINT_ONELINE, &desc)) { gnutls_x509_crt_deinit (cert); goto error; } gnutls_x509_crt_deinit (cert); val = vlc_dialog_wait_question(creds, VLC_DIALOG_QUESTION_WARNING, _("Abort"), _("Accept 24 hours"), _("Accept permanently"), _("Insecure site"), _("This is the certificate presented by %s:\n%s\n\n" "If in doubt, abort now.\n"), host, desc.data); gnutls_free (desc.data); time_t expiry = 0; switch (val) { case 1: time (&expiry); expiry += 24 * 60 * 60; /* fall through */ case 2: val = gnutls_store_pubkey (NULL, NULL, host, service, GNUTLS_CRT_X509, datum, expiry, 0); if (val) msg_Err(creds, "cannot store X.509 certificate: %s", gnutls_strerror (val)); break; default: goto error; } return 0; error: if (alp != NULL) free(*alp); return -1; } /** * Initializes a client-side TLS credentials. */ static int OpenClient (vlc_tls_creds_t *crd) { gnutls_certificate_credentials_t x509; if (gnutls_Init (VLC_OBJECT(crd))) return VLC_EGENERIC; int val = gnutls_certificate_allocate_credentials (&x509); if (val != 0) { msg_Err (crd, "cannot allocate credentials: %s", gnutls_strerror (val)); return VLC_EGENERIC; } if (var_InheritBool(crd, "gnutls-system-trust")) { val = gnutls_certificate_set_x509_system_trust(x509); if (val < 0) msg_Err(crd, "cannot load trusted Certificate Authorities " "from %s: %s", "system", gnutls_strerror(val)); else msg_Dbg(crd, "loaded %d trusted CAs from %s", val, "system"); } char *dir = var_InheritString(crd, "gnutls-dir-trust"); if (dir != NULL) { val = gnutls_certificate_set_x509_trust_dir(x509, dir, GNUTLS_X509_FMT_PEM); if (val < 0) msg_Err(crd, "cannot load trusted Certificate Authorities " "from %s: %s", dir, gnutls_strerror(val)); else msg_Dbg(crd, "loaded %d trusted CAs from %s", val, dir); free(dir); } gnutls_certificate_set_verify_flags (x509, GNUTLS_VERIFY_ALLOW_X509_V1_CA_CRT); crd->sys = x509; crd->open = gnutls_ClientSessionOpen; crd->handshake = gnutls_ClientHandshake; return VLC_SUCCESS; } static void CloseClient (vlc_tls_creds_t *crd) { gnutls_certificate_credentials_t x509 = crd->sys; gnutls_certificate_free_credentials (x509); } #ifdef ENABLE_SOUT /** * Server-side TLS credentials private data */ typedef struct vlc_tls_creds_sys { gnutls_certificate_credentials_t x509_cred; gnutls_dh_params_t dh_params; } vlc_tls_creds_sys_t; /** * Initializes a server-side TLS session. */ static vlc_tls_t *gnutls_ServerSessionOpen(vlc_tls_creds_t *crd, vlc_tls_t *sk, const char *hostname, const char *const *alpn) { vlc_tls_creds_sys_t *sys = crd->sys; vlc_tls_gnutls_t *priv; assert (hostname == NULL); priv = gnutls_SessionOpen(crd, GNUTLS_SERVER, sys->x509_cred, sk, alpn); return (priv != NULL) ? &priv->tls : NULL; } static int gnutls_ServerHandshake(vlc_tls_creds_t *crd, vlc_tls_t *tls, const char *host, const char *service, char **restrict alp) { vlc_tls_gnutls_t *priv = (vlc_tls_gnutls_t *)tls; (void) host; (void) service; return gnutls_ContinueHandshake(crd, priv, alp); } /** * Allocates a whole server's TLS credentials. */ static int OpenServer (vlc_tls_creds_t *crd, const char *cert, const char *key) { int val; if (gnutls_Init (VLC_OBJECT(crd))) return VLC_EGENERIC; vlc_tls_creds_sys_t *sys = malloc (sizeof (*sys)); if (unlikely(sys == NULL)) return VLC_ENOMEM; /* Sets server's credentials */ val = gnutls_certificate_allocate_credentials (&sys->x509_cred); if (val != 0) { msg_Err (crd, "cannot allocate credentials: %s", gnutls_strerror (val)); free (sys); return VLC_ENOMEM; } block_t *certblock = block_FilePath(cert, false); if (certblock == NULL) { msg_Err (crd, "cannot read certificate chain from %s: %s", cert, vlc_strerror_c(errno)); goto error; } block_t *keyblock = block_FilePath(key, false); if (keyblock == NULL) { msg_Err (crd, "cannot read private key from %s: %s", key, vlc_strerror_c(errno)); block_Release (certblock); goto error; } gnutls_datum_t pub = { .data = certblock->p_buffer, .size = certblock->i_buffer, }, priv = { .data = keyblock->p_buffer, .size = keyblock->i_buffer, }; val = gnutls_certificate_set_x509_key_mem (sys->x509_cred, &pub, &priv, GNUTLS_X509_FMT_PEM); block_Release (keyblock); block_Release (certblock); if (val < 0) { msg_Err (crd, "cannot load X.509 key: %s", gnutls_strerror (val)); goto error; } /* FIXME: * - regenerate these regularly * - support other cipher suites */ val = gnutls_dh_params_init (&sys->dh_params); if (val >= 0) { gnutls_sec_param_t sec = GNUTLS_SEC_PARAM_MEDIUM; unsigned bits = gnutls_sec_param_to_pk_bits (GNUTLS_PK_DH, sec); msg_Dbg (crd, "generating Diffie-Hellman %u-bits parameters...", bits); val = gnutls_dh_params_generate2 (sys->dh_params, bits); if (val == 0) gnutls_certificate_set_dh_params (sys->x509_cred, sys->dh_params); } if (val < 0) { msg_Err (crd, "cannot initialize DHE cipher suites: %s", gnutls_strerror (val)); } msg_Dbg (crd, "ciphers parameters loaded"); crd->sys = sys; crd->open = gnutls_ServerSessionOpen; crd->handshake = gnutls_ServerHandshake; return VLC_SUCCESS; error: gnutls_certificate_free_credentials (sys->x509_cred); free (sys); return VLC_EGENERIC; } /** * Destroys a TLS server object. */ static void CloseServer (vlc_tls_creds_t *crd) { vlc_tls_creds_sys_t *sys = crd->sys; /* all sessions depending on the server are now deinitialized */ gnutls_certificate_free_credentials (sys->x509_cred); gnutls_dh_params_deinit (sys->dh_params); free (sys); } #endif #define SYSTEM_TRUST_TEXT N_("Use system trust database") #define SYSTEM_TRUST_LONGTEXT N_( \ "Trust the root certificates of Certificate Authorities stored in " \ "the operating system trust database to authenticate TLS sessions.") #define DIR_TRUST_TEXT N_("Trust directory") #define DIR_TRUST_LONGTEXT N_( \ "Trust the root certificates of Certificate Authorities stored in " \ "the specified directory to authenticate TLS sessions.") #define PRIORITIES_TEXT N_("TLS cipher priorities") #define PRIORITIES_LONGTEXT N_("Ciphers, key exchange methods, " \ "hash functions and compression methods can be selected. " \ "Refer to GNU TLS documentation for detailed syntax.") static const char *const priorities_values[] = { "PERFORMANCE", "NORMAL", "SECURE128", "SECURE256", }; static const char *const priorities_text[] = { N_("Performance (prioritize faster ciphers)"), N_("Normal"), N_("Secure 128-bits (exclude 256-bits ciphers)"), N_("Secure 256-bits (prioritize 256-bits ciphers)"), }; vlc_module_begin () set_shortname( "GNU TLS" ) set_description( N_("GNU TLS transport layer security") ) set_capability( "tls client", 1 ) set_callbacks( OpenClient, CloseClient ) set_category( CAT_ADVANCED ) set_subcategory( SUBCAT_ADVANCED_NETWORK ) add_bool("gnutls-system-trust", true, SYSTEM_TRUST_TEXT, SYSTEM_TRUST_LONGTEXT, true) add_string("gnutls-dir-trust", NULL, DIR_TRUST_TEXT, DIR_TRUST_LONGTEXT, true) add_string ("gnutls-priorities", "NORMAL", PRIORITIES_TEXT, PRIORITIES_LONGTEXT, false) change_string_list (priorities_values, priorities_text) #ifdef ENABLE_SOUT add_submodule () set_description( N_("GNU TLS server") ) set_capability( "tls server", 1 ) set_category( CAT_ADVANCED ) set_subcategory( SUBCAT_ADVANCED_NETWORK ) set_callbacks( OpenServer, CloseServer ) #endif vlc_module_end ()