diff --git a/src/ice_transport.cpp b/src/ice_transport.cpp index f0e7b7ba0763b87360cb48c3b4766315aad89d5d..34b73a09d96cec187b3b1855b9982db6da6fe0a6 100644 --- a/src/ice_transport.cpp +++ b/src/ice_transport.cpp @@ -853,9 +853,11 @@ IceTransport::waitForData(int comp_id, unsigned int timeout) auto& io = compIO_[comp_id]; std::unique_lock<std::mutex> lk(io.mutex); if (!io.cv.wait_for(lk, std::chrono::milliseconds(timeout), - [&io]{ return !io.queue.empty(); })) { + [this, &io]{ return !io.queue.empty() or !isRunning(); })) { return 0; } + if (!isRunning()) + return -1; // acknowledged as an error return io.queue.front().datalen; } diff --git a/src/ringdht/ringaccount.cpp b/src/ringdht/ringaccount.cpp index 68f3e160627fa024f84d874a7158a3c75d1266c0..86a9d7d9b1bf2a824887aef51aecf0f5b2dec10b 100644 --- a/src/ringdht/ringaccount.cpp +++ b/src/ringdht/ringaccount.cpp @@ -665,9 +665,12 @@ RingAccount::handlePendingCall(PendingCall& pc, bool incoming) // Securize a SIP transport with TLS (on top of ICE tranport) and assign the call with it auto remote_h = pc.from; + auto id(loadIdentity()); + tls::TlsParams tlsParams { .ca_list = "", - .id = loadIdentity(), + .cert = id.second, + .cert_key = id.first, .dh_params = dhParams_, .timeout = std::chrono::duration_cast<decltype(tls::TlsParams::timeout)>(TLS_TIMEOUT), .cert_check = [remote_h](unsigned status, const gnutls_datum_t* cert_list, @@ -1209,36 +1212,10 @@ RingAccount::loadValues() const return values; } -static std::unique_ptr<gnutls_dh_params_int, decltype(gnutls_dh_params_deinit)&> -getNewDhParams() -{ - using namespace std::chrono; - auto bits = gnutls_sec_param_to_pk_bits(GNUTLS_PK_DH, /* GNUTLS_SEC_PARAM_HIGH */ GNUTLS_SEC_PARAM_NORMAL); - RING_DBG("Generating DH params with %u bits", bits); - auto t1 = high_resolution_clock::now(); - - gnutls_dh_params_t new_params_; - int ret = gnutls_dh_params_init(&new_params_); - if (ret != GNUTLS_E_SUCCESS) { - RING_ERR("Error initializing DH params: %s", gnutls_strerror(ret)); - return {nullptr, gnutls_dh_params_deinit}; - } - - ret = gnutls_dh_params_generate2(new_params_, bits); - if (ret != GNUTLS_E_SUCCESS) { - RING_ERR("Error generating DH params: %s", gnutls_strerror(ret)); - return {nullptr, gnutls_dh_params_deinit}; - } - - auto time_span = duration_cast<duration<double>>(high_resolution_clock::now() - t1); - RING_DBG("Generated DH params with %u bits in %lfs", bits, time_span.count()); - return {new_params_, gnutls_dh_params_deinit}; -} - void RingAccount::generateDhParams() { - std::packaged_task<decltype(getNewDhParams())()> task(&getNewDhParams); + std::packaged_task<decltype(tls::newDhParams)> task(tls::newDhParams); dhParams_ = task.get_future(); std::thread task_td(std::move(task)); task_td.detach(); diff --git a/src/ringdht/ringaccount.h b/src/ringdht/ringaccount.h index f9d1190b7289d947a18497bda763b2432136cf2e..e8c600a3f8528240c611c50e3da5c9392655ec2c 100644 --- a/src/ringdht/ringaccount.h +++ b/src/ringdht/ringaccount.h @@ -24,6 +24,7 @@ #include "config.h" #endif +#include "security/tls_session.h" #include "sip/sipaccountbase.h" #include "noncopyable.h" @@ -374,7 +375,7 @@ class RingAccount : public SIPAccountBase { */ void generateDhParams(); - std::shared_future<std::unique_ptr<gnutls_dh_params_int, decltype(gnutls_dh_params_deinit)&>> dhParams_; + std::shared_future<tls::TlsParams::DhParams> dhParams_; std::mutex dhParamsMtx_; std::condition_variable dhParamsCv_; bool allowPeersFromHistory_; diff --git a/src/ringdht/sips_transport_ice.cpp b/src/ringdht/sips_transport_ice.cpp index b62d3f88aaec9977cb1dbecdd51153f05888301f..0cef35de9ce481dac7667601cb174430c0a6b5ee 100644 --- a/src/ringdht/sips_transport_ice.cpp +++ b/src/ringdht/sips_transport_ice.cpp @@ -1,7 +1,8 @@ /* - * Copyright (C) 2004-2015 Savoir-faire Linux Inc. + * Copyright (C) 2004-2016 Savoir-faire Linux Inc. * * Author: Adrien Béraud <adrien.beraud@savoirfairelinux.com> + * Author: Guillaume Roguez <guillaume.roguez@savoirfairelinux.com> * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -19,9 +20,14 @@ */ #include "sips_transport_ice.h" + #include "ice_transport.h" #include "manager.h" +#include "sip/sip_utils.h" #include "logger.h" +#include "intrin.h" + +#include <opendht/crypto.h> #include <gnutls/dtls.h> #include <gnutls/abstract.h> @@ -39,7 +45,6 @@ namespace ring { namespace tls { static constexpr int POOL_TP_INIT {512}; static constexpr int POOL_TP_INC {512}; static constexpr int TRANSPORT_INFO_LENGTH {64}; -static constexpr int DTLS_MTU {6400}; static void sockaddr_to_host_port(pj_pool_t* pool, @@ -52,74 +57,138 @@ sockaddr_to_host_port(pj_pool_t* pool, host_port->port = pj_sockaddr_get_port(addr); } +static pj_status_t +tls_status_from_err(int err) +{ + pj_status_t status; + + switch (err) { + case GNUTLS_E_SUCCESS: + status = PJ_SUCCESS; + break; + case GNUTLS_E_MEMORY_ERROR: + status = PJ_ENOMEM; + break; + case GNUTLS_E_LARGE_PACKET: + status = PJ_ETOOBIG; + break; + case GNUTLS_E_NO_CERTIFICATE_FOUND: + status = PJ_ENOTFOUND; + break; + case GNUTLS_E_SESSION_EOF: + status = PJ_EEOF; + break; + case GNUTLS_E_HANDSHAKE_TOO_LARGE: + status = PJ_ETOOBIG; + break; + case GNUTLS_E_EXPIRED: + status = PJ_EGONE; + break; + case GNUTLS_E_TIMEDOUT: + status = PJ_ETIMEDOUT; + break; + case GNUTLS_E_PREMATURE_TERMINATION: + status = PJ_ECANCELLED; + break; + case GNUTLS_E_INTERNAL_ERROR: + case GNUTLS_E_UNIMPLEMENTED_FEATURE: + status = PJ_EBUG; + break; + case GNUTLS_E_AGAIN: + case GNUTLS_E_INTERRUPTED: + case GNUTLS_E_REHANDSHAKE: + status = PJ_EPENDING; + break; + case GNUTLS_E_TOO_MANY_EMPTY_PACKETS: + case GNUTLS_E_TOO_MANY_HANDSHAKE_PACKETS: + case GNUTLS_E_RECORD_LIMIT_REACHED: + status = PJ_ETOOMANY; + break; + case GNUTLS_E_UNSUPPORTED_VERSION_PACKET: + case GNUTLS_E_UNSUPPORTED_SIGNATURE_ALGORITHM: + case GNUTLS_E_UNSUPPORTED_CERTIFICATE_TYPE: + case GNUTLS_E_X509_UNSUPPORTED_ATTRIBUTE: + case GNUTLS_E_X509_UNSUPPORTED_EXTENSION: + case GNUTLS_E_X509_UNSUPPORTED_CRITICAL_EXTENSION: + status = PJ_ENOTSUP; + break; + case GNUTLS_E_INVALID_SESSION: + case GNUTLS_E_INVALID_REQUEST: + case GNUTLS_E_INVALID_PASSWORD: + case GNUTLS_E_ILLEGAL_PARAMETER: + case GNUTLS_E_RECEIVED_ILLEGAL_EXTENSION: + case GNUTLS_E_UNEXPECTED_PACKET: + case GNUTLS_E_UNEXPECTED_PACKET_LENGTH: + case GNUTLS_E_UNEXPECTED_HANDSHAKE_PACKET: + case GNUTLS_E_UNWANTED_ALGORITHM: + case GNUTLS_E_USER_ERROR: + status = PJ_EINVAL; + break; + default: + status = PJ_EUNKNOWN; + break; + } + + return status; +} + SipsIceTransport::SipsIceTransport(pjsip_endpoint* endpt, const TlsParams& param, const std::shared_ptr<ring::IceTransport>& ice, int comp_id) - : pool_ {nullptr, pj_pool_release} - , rxPool_ (nullptr, pj_pool_release) - , trData_ () - , ice_ (ice) + : ice_ (ice) , comp_id_ (comp_id) - , param_ (param) - , tlsThread_( - std::bind(&SipsIceTransport::setup, this), - std::bind(&SipsIceTransport::loop, this), - std::bind(&SipsIceTransport::clean, this)) - , xcred_ {nullptr, gnutls_certificate_free_credentials} + , certCheck_(param.cert_check) + , trData_ () + , pool_ {nullptr, pj_pool_release} + , rxPool_ (nullptr, pj_pool_release) { RING_DBG("SipIceTransport@%p {tr=%p}", this, &trData_.base); - trData_.self = this; - if (not ice or not ice->isRunning()) throw std::logic_error("ICE transport must exist and negotiation completed"); - pool_.reset(pjsip_endpt_create_pool(endpt, "SipsIceTransport.pool", - POOL_TP_INIT, POOL_TP_INC)); - if (not pool_) { - RING_ERR("Can't create PJSIP pool"); - throw std::bad_alloc(); - } - auto pool = pool_.get(); + trData_.self = this; // up-link for PJSIP callbacks + + pool_ = std::move(sip_utils::smart_alloc_pool(endpt, "SipsIceTransport.pool", + POOL_TP_INIT, POOL_TP_INC)); auto& base = trData_.base; + std::memset(&rdata_, 0, sizeof(pjsip_rx_data)); + pj_ansi_snprintf(base.obj_name, PJ_MAX_OBJ_NAME, "SipsIceTransport"); base.endpt = endpt; base.tpmgr = pjsip_endpt_get_tpmgr(endpt); - base.pool = pool; + base.pool = pool_.get(); - if (pj_atomic_create(pool, 0, &base.ref_cnt) != PJ_SUCCESS) + if (pj_atomic_create(pool_.get(), 0, &base.ref_cnt) != PJ_SUCCESS) throw std::runtime_error("Can't create PJSIP atomic."); - if (pj_lock_create_recursive_mutex(pool, "SipsIceTransport.mutex", + if (pj_lock_create_recursive_mutex(pool_.get(), "SipsIceTransport.mutex", &base.lock) != PJ_SUCCESS) throw std::runtime_error("Can't create PJSIP mutex."); - is_server_ = not ice->isInitiator(); local_ = ice->getLocalAddress(comp_id); remote_ = ice->getRemoteAddress(comp_id); pj_sockaddr_cp(&base.key.rem_addr, remote_.pjPtr()); base.key.type = PJSIP_TRANSPORT_TLS; base.type_name = (char*)pjsip_transport_get_type_name((pjsip_transport_type_e)base.key.type); base.flag = pjsip_transport_get_flag_from_type((pjsip_transport_type_e)base.key.type); - base.info = (char*) pj_pool_alloc(pool, TRANSPORT_INFO_LENGTH); + base.info = (char*) pj_pool_alloc(pool_.get(), TRANSPORT_INFO_LENGTH); char print_addr[PJ_INET6_ADDRSTRLEN+10]; - pj_ansi_snprintf(base.info, TRANSPORT_INFO_LENGTH, "%s to %s", - base.type_name, - pj_sockaddr_print(remote_.pjPtr(), print_addr, - sizeof(print_addr), 3)); + pj_ansi_snprintf(base.info, TRANSPORT_INFO_LENGTH, "%s to %s", base.type_name, + pj_sockaddr_print(remote_.pjPtr(), print_addr, sizeof(print_addr), 3)); base.addr_len = remote_.getLength(); - base.dir = PJSIP_TP_DIR_NONE; ///is_server? PJSIP_TP_DIR_INCOMING : PJSIP_TP_DIR_OUTGOING; + base.dir = PJSIP_TP_DIR_NONE; base.data = nullptr; /* Set initial local address */ auto local = ice->getDefaultLocalAddress(); pj_sockaddr_cp(&base.local_addr, local.pjPtr()); - sockaddr_to_host_port(pool, &base.local_name, &base.local_addr); - sockaddr_to_host_port(pool, &base.remote_name, remote_.pjPtr()); + sockaddr_to_host_port(pool_.get(), &base.local_name, &base.local_addr); + sockaddr_to_host_port(pool_.get(), &base.remote_name, remote_.pjPtr()); base.send_msg = [](pjsip_transport *transport, pjsip_tx_data *tdata, @@ -131,26 +200,31 @@ SipsIceTransport::SipsIceTransport(pjsip_endpoint* endpt, base.do_shutdown = [](pjsip_transport *transport) -> pj_status_t { auto& this_ = reinterpret_cast<TransportData*>(transport)->self; RING_DBG("SipsIceTransport@%p: shutdown", this_); - this_->shutdown(); + { + // Flush pending state changes and rx packet before shutdown + // or pjsip callbacks will crash + + std::lock_guard<std::mutex> lk{this_->stateChangeEventsMutex_}; + this_->stateChangeEvents_.clear(); + + std::lock_guard<std::mutex> lk2(this_->rxMtx_); + this_->rxPending_.clear(); + + this_->tls_->shutdown(); + } return PJ_SUCCESS; }; base.destroy = [](pjsip_transport *transport) -> pj_status_t { auto& this_ = reinterpret_cast<TransportData*>(transport)->self; - RING_DBG("SipsIceTransport@%p: destroy", this_); + RING_DBG("SipsIceTransport@%p: destroying", this_); delete this_; return PJ_SUCCESS; }; /* Init rdata_ */ std::memset(&rdata_, 0, sizeof(pjsip_rx_data)); - rxPool_.reset(pjsip_endpt_create_pool(base.endpt, - "SipsIceTransport.rxPool", - PJSIP_POOL_RDATA_LEN, - PJSIP_POOL_RDATA_LEN)); - if (not rxPool_) { - RING_ERR("Can't create PJSIP rx pool"); - throw std::bad_alloc(); - } + rxPool_ = std::move(sip_utils::smart_alloc_pool(endpt, "SipsIceTransport.rxPool", + PJSIP_POOL_RDATA_LEN, PJSIP_POOL_RDATA_LEN)); rdata_.tp_info.pool = rxPool_.get(); rdata_.tp_info.transport = &base; rdata_.tp_info.tp_data = this; @@ -167,210 +241,249 @@ SipsIceTransport::SipsIceTransport(pjsip_endpoint* endpt, std::memset(&localCertInfo_, 0, sizeof(pj_ssl_cert_info)); std::memset(&remoteCertInfo_, 0, sizeof(pj_ssl_cert_info)); - /* Register error subsystem */ - /*pj_status_t status = pj_register_strerror(PJ_ERRNO_START_USER + - PJ_ERRNO_SPACE_SIZE * 6, - PJ_ERRNO_SPACE_SIZE, - &tls_strerror); - pj_assert(status == PJ_SUCCESS);*/ + TlsSession::TlsSessionCallbacks cbs = { + .onStateChange = [this](TlsSessionState state){ onTlsStateChange(state); }, + .onRxData = [this](std::vector<uint8_t>&& buf){ onRxData(std::move(buf)); }, + .onCertificatesUpdate = [this](const gnutls_datum_t* l, const gnutls_datum_t* r, + unsigned int n){ onCertificatesUpdate(l, r, n); }, + .verifyCertificate = [this](gnutls_session_t session){ return verifyCertificate(session); } + }; + tls_.reset(new TlsSession(ice, comp_id, param, cbs)); if (pjsip_transport_register(base.tpmgr, &base) != PJ_SUCCESS) throw std::runtime_error("Can't register PJSIP transport."); - gnutls_priority_init(&priority_cache_, - "SECURE192:-VERS-TLS-ALL:+VERS-DTLS-ALL:%SERVER_PRECEDENCE", - nullptr); - - tlsThread_.start(); Manager::instance().registerEventHandler((uintptr_t)this, [this]{ handleEvents(); }); } SipsIceTransport::~SipsIceTransport() { - RING_DBG("~SipsIceTransport"); - auto event = state_ == TlsConnectionState::ESTABLISHED; - shutdown(); - tlsThread_.join(); - Manager::instance().unregisterEventHandler((uintptr_t)this); - handleEvents(); // process latest incoming packets - - pjsip_transport_add_ref(getTransportBase()); - auto state_cb = pjsip_tpmgr_get_state_cb(trData_.base.tpmgr); - if (state_cb && event) { - pjsip_transport_state_info state_info; - pjsip_tls_state_info tls_info; - - /* Init transport state info */ - std::memset(&state_info, 0, sizeof(state_info)); - std::memset(&tls_info, 0, sizeof(tls_info)); - pj_ssl_sock_info ssl_info; - getInfo(&ssl_info); - tls_info.ssl_sock_info = &ssl_info; - state_info.ext_info = &tls_info; - state_info.status = PJ_SUCCESS; - - (*state_cb)(getTransportBase(), PJSIP_TP_STATE_DISCONNECTED, &state_info); - } + RING_DBG("~SipIceTransport@%p {tr=%p}", this, &trData_.base); - if (not trData_.base.is_shutdown and not trData_.base.is_destroying) - pjsip_transport_shutdown(getTransportBase()); + auto base = getTransportBase(); + Manager::instance().unregisterEventHandler((uintptr_t)this); - pjsip_transport_dec_ref(getTransportBase()); + // Stop low-level transport first + tls_.reset(); - pj_lock_destroy(trData_.base.lock); - pj_atomic_destroy(trData_.base.ref_cnt); + // If delete not trigged by pjsip_transport_destroy (happen if objet not given to pjsip) + if (not base->is_shutdown and not base->is_destroying) + pjsip_transport_shutdown(base); - gnutls_priority_deinit(priority_cache_); + pj_lock_destroy(base->lock); + pj_atomic_destroy(base->ref_cnt); + RING_DBG("~SipIceTransport@%p {tr=%p} bye", this, &trData_.base); } -pj_status_t -SipsIceTransport::startTlsSession() +void +SipsIceTransport::handleEvents() { - RING_DBG("SipsIceTransport::startTlsSession as %s", - (is_server_ ? "server" : "client")); - int ret; + // Notify transport manager about state changes first + decltype(stateChangeEvents_) eventDataQueue; + { + std::lock_guard<std::mutex> lk{stateChangeEventsMutex_}; + eventDataQueue = std::move(stateChangeEvents_); + stateChangeEvents_.clear(); + } - ret = gnutls_init(&session_, (is_server_ ? GNUTLS_SERVER : GNUTLS_CLIENT) | GNUTLS_DATAGRAM); - if (ret != GNUTLS_E_SUCCESS) { - shutdown(); - return tls_status_from_err(ret); + if (auto state_cb = pjsip_tpmgr_get_state_cb(trData_.base.tpmgr)) { + for (auto& evdata : eventDataQueue) { + evdata.tls_info.ssl_sock_info = &evdata.ssl_info; + evdata.state_info.ext_info = &evdata.tls_info; + (*state_cb)(&trData_.base, evdata.state, &evdata.state_info); + } + } + + // Handle TLS -> SIP transport + decltype(rxPending_) rx; + { + std::lock_guard<std::mutex> l(rxMtx_); + rx = std::move(rxPending_); + rxPending_.clear(); } - gnutls_session_set_ptr(session_, this); - gnutls_transport_set_ptr(session_, this); + for (auto it = rx.begin(); it != rx.end(); ++it) { + auto& pck = *it; + pj_pool_reset(rdata_.tp_info.pool); + pj_gettimeofday(&rdata_.pkt_info.timestamp); + rdata_.pkt_info.len = pck.size(); + std::copy_n(pck.data(), pck.size(), rdata_.pkt_info.packet); + auto eaten = pjsip_tpmgr_receive_packet(trData_.base.tpmgr, &rdata_); - gnutls_priority_set(session_, priority_cache_); + // Uncomplet parsing? (may be a partial sip packet received) + if (eaten != rdata_.pkt_info.len) { + auto npck_it = std::next(it); + if (npck_it != rx.end()) { + // drop current packet, merge reminder with next one + auto& npck = *npck_it; + npck.insert(npck.begin(), pck.begin()+eaten, pck.end()); + } else { + // erase eaten part, keep reminder + pck.erase(pck.begin(), pck.begin()+eaten); + { + std::lock_guard<std::mutex> l(rxMtx_); + rxPending_.splice(rxPending_.begin(), rx, it); + } + break; + } + } + } - /* Allocate credentials for handshaking and transmission */ - gnutls_certificate_credentials_t certCred; - ret = gnutls_certificate_allocate_credentials(&certCred); - if (ret < 0 or not certCred) { - RING_ERR("Can't allocate credentials"); - shutdown(); - return PJ_ENOMEM; + // Acknowledged SIP -> TLS packet + decltype(txAckQueue_) ack_queue; + { + std::lock_guard<std::mutex> l(txAckQueueMutex_); + ack_queue = std::move(txAckQueue_); + txAckQueue_.clear(); + } + for (const auto& pair: ack_queue) { + auto tdata = pair.first; + tdata->op_key.tdata = nullptr; + if (tdata->op_key.callback) + tdata->op_key.callback(&trData_.base, tdata->op_key.token, pair.second); } +} - xcred_.reset(certCred); +void +SipsIceTransport::pushChangeStateEvent(ChangeStateEventData&& ev) +{ + std::lock_guard<std::mutex> lk{stateChangeEventsMutex_}; + stateChangeEvents_.emplace_back(std::move(ev)); +} + +// - DO NOT BLOCK - (Called in TlsSession thread) +void +SipsIceTransport::onTlsStateChange(UNUSED TlsSessionState state) +{ + if (state == TlsSessionState::ESTABLISHED) + updateTransportState(PJSIP_TP_STATE_CONNECTED); + else if (state == TlsSessionState::SHUTDOWN) + updateTransportState(PJSIP_TP_STATE_DISCONNECTED); +} - if (is_server_) { - auto& dh_params = param_.dh_params.get(); - if (dh_params) - gnutls_certificate_set_dh_params(certCred, dh_params.get()); - else - RING_ERR("DH params unavaliable"); +// - DO NOT BLOCK - (Called in TlsSession thread) +void +SipsIceTransport::onRxData(std::vector<uint8_t>&& buf) +{ + std::lock_guard<std::mutex> l(rxMtx_); + if (rxPending_.empty()) + rxPending_.emplace_back(std::move(buf)); + else { + auto& last = rxPending_.back(); + last.insert(std::end(last), std::begin(buf), std::end(buf)); } +} - gnutls_certificate_set_verify_function(certCred, [](gnutls_session_t session) { - auto this_ = reinterpret_cast<SipsIceTransport*>(gnutls_session_get_ptr(session)); - return this_->verifyCertificate(); - }); - - if (not param_.ca_list.empty()) { - /* Load CA if one is specified. */ - ret = gnutls_certificate_set_x509_trust_file(certCred, - param_.ca_list.c_str(), - GNUTLS_X509_FMT_PEM); - if (ret < 0) - ret = gnutls_certificate_set_x509_trust_file(certCred, - param_.ca_list.c_str(), - GNUTLS_X509_FMT_DER); - if (ret < 0) - throw std::runtime_error("Can't load CA."); - RING_DBG("Loaded %s", param_.ca_list.c_str()); +/* Update local & remote certificates info. This function should be + * called after handshake or re-negotiation successfully completed. + * + * - DO NOT BLOCK - (Called in TlsSession thread) + */ +void +SipsIceTransport::onCertificatesUpdate(const gnutls_datum_t* local_raw, + const gnutls_datum_t* remote_raw, + unsigned int remote_count) +{ + // local certificate + if (local_raw) + certGetInfo(pool_.get(), &localCertInfo_, local_raw, 1); + else + std::memset(&localCertInfo_, 0, sizeof(pj_ssl_cert_info)); + + // Remote certificates + if (remote_raw) + certGetInfo(pool_.get(), &remoteCertInfo_, remote_raw, remote_count); + else + std::memset(&remoteCertInfo_, 0, sizeof(pj_ssl_cert_info)); +} + +int +SipsIceTransport::verifyCertificate(gnutls_session_t session) +{ + // Support only x509 format + if (gnutls_certificate_type_get(session) != GNUTLS_CRT_X509) { + verifyStatus_ = PJ_SSL_CERT_EINVALID_FORMAT; + return GNUTLS_E_CERTIFICATE_ERROR; } - if (param_.id.first) { - /* Load certificate, key and pass */ - ret = gnutls_certificate_set_x509_key(certCred, - ¶m_.id.second->cert, 1, - param_.id.first->x509_key); - if (ret < 0) - throw std::runtime_error("Can't load certificate : " - + std::string(gnutls_strerror(ret))); + + // Store verification status + unsigned int status = 0; + auto ret = gnutls_certificate_verify_peers2(session, &status); + if (ret < 0 or (status & GNUTLS_CERT_SIGNATURE_FAILURE) != 0) { + verifyStatus_ = PJ_SSL_CERT_EUNTRUSTED; + return GNUTLS_E_CERTIFICATE_ERROR; } - /* Finally set credentials for this session */ - ret = gnutls_credentials_set(session_, GNUTLS_CRD_CERTIFICATE, certCred); - if (ret != GNUTLS_E_SUCCESS) { - shutdown(); - return tls_status_from_err(ret); + unsigned int cert_list_size = 0; + auto cert_list = gnutls_certificate_get_peers(session, &cert_list_size); + if (cert_list == nullptr) { + verifyStatus_ = PJ_SSL_CERT_EISSUER_NOT_FOUND; + return GNUTLS_E_CERTIFICATE_ERROR; } - if (is_server_) { - /* Require client certificate and valid cookie */ - gnutls_certificate_server_set_request(session_, GNUTLS_CERT_REQUIRE); - gnutls_dtls_prestate_set(session_, &prestate_); + if (certCheck_) { + verifyStatus_ = certCheck_(status, cert_list, cert_list_size); + if (verifyStatus_ != PJ_SUCCESS) + return GNUTLS_E_CERTIFICATE_ERROR; } - gnutls_dtls_set_mtu(session_, DTLS_MTU); - - gnutls_transport_set_vec_push_function(session_, [](gnutls_transport_ptr_t t, - const giovec_t* iov, - int iovcnt) -> ssize_t { - auto this_ = reinterpret_cast<SipsIceTransport*>(t); - ssize_t sent = 0; - for (int i=0; i<iovcnt; i++) { - const giovec_t& dat = *(iov+i); - auto ret = this_->tlsSend(dat.iov_base, dat.iov_len); - if (ret < 0) - return ret; - sent += ret; - } - return sent; - }); - gnutls_transport_set_pull_function(session_, [](gnutls_transport_ptr_t t, - void* d, - size_t s) -> ssize_t { - auto this_ = reinterpret_cast<SipsIceTransport*>(t); - return this_->tlsRecv(d, s); - }); - gnutls_transport_set_pull_timeout_function(session_, [](gnutls_transport_ptr_t t, - unsigned ms) -> int { - auto this_ = reinterpret_cast<SipsIceTransport*>(t); - return this_->waitForTlsData(ms); - }); - - // start handshake - handshakeStart_ = clock::now(); - state_ = TlsConnectionState::HANDSHAKING; - - return PJ_SUCCESS; + // notify GnuTLS to continue handshake normally + return GNUTLS_E_SUCCESS; } void -SipsIceTransport::closeTlsSession() +SipsIceTransport::updateTransportState(pjsip_transport_state state) { - state_ = TlsConnectionState::DISCONNECTED; - if (session_) { - gnutls_bye(session_, GNUTLS_SHUT_RDWR); - gnutls_deinit(session_); - session_ = nullptr; - } + ChangeStateEventData ev; + + std::memset(&ev.state_info, 0, sizeof(ev.state_info)); + std::memset(&ev.tls_info, 0, sizeof(ev.tls_info)); - xcred_.reset(); + ev.state = state; + tlsConnected_ = state == PJSIP_TP_STATE_CONNECTED; + getInfo(&ev.ssl_info, tlsConnected_); + if (tlsConnected_) + ev.state_info.status = ev.ssl_info.verify_status ? PJSIP_TLS_ECERTVERIF : PJ_SUCCESS; + else + ev.state_info.status = PJ_SUCCESS; // TODO: use last gnu error + + pushChangeStateEvent(std::move(ev)); } void -SipsIceTransport::certGetCn(const pj_str_t* gen_name, pj_str_t* cn) +SipsIceTransport::getInfo(pj_ssl_sock_info* info, bool established) { - pj_str_t CN_sign = {(char*)"CN=", 3}; - char *p, *q; + std::memset(info, 0, sizeof(*info)); - std::memset(cn, 0, sizeof(*cn)); + info->established = established; + info->proto = PJ_SSL_SOCK_PROTO_DTLS1; - p = pj_strstr(gen_name, &CN_sign); - if (!p) - return; + pj_sockaddr_cp(&info->local_addr, local_.pjPtr()); - p += 3; /* shift pointer to value part */ - pj_strset(cn, p, gen_name->slen - (p - gen_name->ptr)); - q = pj_strchr(cn, ','); - if (q) - cn->slen = q - p; + if (established) { + // Cipher Suite Id + std::array<uint8_t, 2> cs_id; + if (auto cipher_name = tls_->getCurrentCipherSuiteId(cs_id)) { + info->cipher = static_cast<pj_ssl_cipher>((cs_id[0] << 8) | cs_id[1]); + RING_DBG("[TLS] using cipher %s (0x%02X%02X)", cipher_name, cs_id[0], cs_id[1]); + } else + RING_ERR("[TLS] Can't find info on used cipher"); + + info->local_cert_info = &localCertInfo_; + info->remote_cert_info = &remoteCertInfo_; + info->verify_status = verifyStatus_; + + pj_sockaddr_cp(&info->remote_addr, remote_.pjPtr()); + } + + // Last known GnuTLS error code + info->last_native_err = GNUTLS_E_SUCCESS; } /* Get certificate info; in case the certificate info is already populated, * this function will check if the contents need updating by inspecting the - * issuer and the serial number. */ + * issuer and the serial number. + */ void SipsIceTransport::certGetInfo(pj_pool_t* pool, pj_ssl_cert_info* ci, const gnutls_datum_t* crt_raw, size_t crt_raw_num) @@ -441,7 +554,7 @@ SipsIceTransport::certGetInfo(pj_pool_t* pool, pj_ssl_cert_info* ci, ci->subj_alt_name.entry = \ (decltype(ci->subj_alt_name.entry))pj_pool_calloc(pool, seq, sizeof(*ci->subj_alt_name.entry)); if (!ci->subj_alt_name.entry) { - last_err_ = GNUTLS_E_MEMORY_ERROR; + //last_err_ = GNUTLS_E_MEMORY_ERROR; return; } @@ -482,471 +595,23 @@ SipsIceTransport::certGetInfo(pj_pool_t* pool, pj_ssl_cert_info* ci, } } - -/* Update local & remote certificates info. This function should be - * called after handshake or renegotiation successfully completed. */ -void -SipsIceTransport::certUpdate() -{ - /* Get active local certificate */ - if(const auto local_raw = gnutls_certificate_get_ours(session_)) - certGetInfo(pool_.get(), &localCertInfo_, local_raw, 1); - else - std::memset(&localCertInfo_, 0, sizeof(pj_ssl_cert_info)); - - unsigned int certslen = 0; - if (const auto remote_raw = gnutls_certificate_get_peers(session_, &certslen)) - certGetInfo(pool_.get(), &remoteCertInfo_, remote_raw, certslen); - else - std::memset(&remoteCertInfo_, 0, sizeof(pj_ssl_cert_info)); -} - void -SipsIceTransport::getInfo(pj_ssl_sock_info* info) -{ - std::memset(info, 0, sizeof(*info)); - - /* Established flag */ - info->established = (state_ == TlsConnectionState::ESTABLISHED); - - /* Protocol */ - info->proto = PJ_SSL_SOCK_PROTO_DTLS1; - - /* Local address */ - pj_sockaddr_cp(&info->local_addr, local_.pjPtr()); - - if (info->established) { - unsigned char id[2]; - gnutls_cipher_algorithm_t cipher; - gnutls_cipher_algorithm_t lookup; - - /* Current cipher */ - cipher = gnutls_cipher_get(session_); - for (size_t i=0; ; ++i) { - const auto suite = gnutls_cipher_suite_info(i, id, nullptr, &lookup, - nullptr, nullptr); - if (not suite) { - RING_ERR("Can't find info for cipher %s (%d)", gnutls_cipher_get_name(cipher), cipher); - break; - } - - if (lookup == cipher) { - info->cipher = (pj_ssl_cipher) ((id[0] << 8) | id[1]); - break; - } - } - - /* Remote address */ - pj_sockaddr_cp(&info->remote_addr, remote_.pjPtr()); - - /* Certificates info */ - info->local_cert_info = &localCertInfo_; - info->remote_cert_info = &remoteCertInfo_; - - /* Verification status */ - info->verify_status = verifyStatus_; - } - - /* Last known GnuTLS error code */ - info->last_native_err = last_err_; -} - -pj_status_t -SipsIceTransport::tryHandshake() -{ - RING_DBG("SipsIceTransport::tryHandshake as %s", - (is_server_ ? "server" : "client")); - pj_status_t status; - int ret = gnutls_handshake(session_); - if (ret == GNUTLS_E_SUCCESS) { - /* System are GO */ - RING_DBG("SipsIceTransport::tryHandshake : ESTABLISHED"); - state_ = TlsConnectionState::ESTABLISHED; - status = PJ_SUCCESS; - } else if (!gnutls_error_is_fatal(ret)) { - /* Non fatal error, retry later (busy or again) */ - status = PJ_EPENDING; - } else { - /* Fatal error invalidates session, no fallback */ - RING_ERR("TLS fatal error : %s", gnutls_strerror(ret)); - status = PJ_EINVAL; - } - last_err_ = ret; - return status; -} - -/* When handshake completed: - * - notify application - * - if handshake failed, reset SSL state - * - return false when SSL socket instance is destroyed by application. */ -bool -SipsIceTransport::onHandshakeComplete(pj_status_t status) -{ - /* Update certificates info on successful handshake */ - if (status == PJ_SUCCESS) { - RING_DBG("Handshake success on remote %s", - remote_.toString(true).c_str()); - certUpdate(); - } else { - /* Handshake failed destroy ourself silently. */ - char errmsg[PJ_ERR_MSG_SIZE]; - RING_ERR("Handshake failed on remote %s: %s", - remote_.toString(true).c_str(), - pj_strerror(status, errmsg, sizeof(errmsg)).ptr); - } - - ChangeStateEventData eventData; - getInfo(&eventData.ssl_info); - - if (status != PJ_SUCCESS) - shutdown(); // to be done after getInfo() call - - // push event to handleEvents() - eventData.state_info.status = eventData.ssl_info.verify_status ? PJSIP_TLS_ECERTVERIF : PJ_SUCCESS; - eventData.state = (status != PJ_SUCCESS) ? PJSIP_TP_STATE_DISCONNECTED : PJSIP_TP_STATE_CONNECTED; - - { - std::lock_guard<std::mutex> lk{stateChangeEventsMutex_}; - stateChangeEvents_.emplace(std::move(eventData)); - } - - return status == PJ_SUCCESS; -} - -int -SipsIceTransport::verifyCertificate() -{ - /* Support only x509 format */ - if (gnutls_certificate_type_get(session_) != GNUTLS_CRT_X509) { - verifyStatus_ = PJ_SSL_CERT_EINVALID_FORMAT; - return GNUTLS_E_CERTIFICATE_ERROR; - } - - /* Store verification status */ - unsigned int status = 0; - auto ret = gnutls_certificate_verify_peers2(session_, &status); - if (ret < 0 or (status & GNUTLS_CERT_SIGNATURE_FAILURE) != 0) { - verifyStatus_ = PJ_SSL_CERT_EUNTRUSTED; - return GNUTLS_E_CERTIFICATE_ERROR; - } - - unsigned int cert_list_size = 0; - auto cert_list = gnutls_certificate_get_peers(session_, &cert_list_size); - if (cert_list == nullptr) { - verifyStatus_ = PJ_SSL_CERT_EISSUER_NOT_FOUND; - return GNUTLS_E_CERTIFICATE_ERROR; - } - - if (param_.cert_check) { - verifyStatus_ = param_.cert_check(status, cert_list, cert_list_size); - if (verifyStatus_ != PJ_SUCCESS) - return GNUTLS_E_CERTIFICATE_ERROR; - } - - /* notify GnuTLS to continue handshake normally */ - return GNUTLS_E_SUCCESS; -} - -void -SipsIceTransport::handleEvents() -{ - // Process state changes first - decltype(stateChangeEvents_) eventDataQueue = stateChangeEvents_; - { - std::lock_guard<std::mutex> lk{stateChangeEventsMutex_}; - eventDataQueue = std::move(stateChangeEvents_); - } - if (auto state_cb = pjsip_tpmgr_get_state_cb(trData_.base.tpmgr)) { - // application notification - while (not eventDataQueue.empty()) { - auto& evdata = eventDataQueue.front(); - evdata.tls_info.ssl_sock_info = &evdata.ssl_info; - evdata.state_info.ext_info = &evdata.tls_info; - (*state_cb)(&trData_.base, evdata.state, &evdata.state_info); - eventDataQueue.pop(); - } - } - - // Handle stream GnuTLS -> SIP transport - decltype(rxPending_) rx; - { - std::lock_guard<std::mutex> l(rxMtx_); - rx = std::move(rxPending_); - } - - for (auto it = rx.begin(); it != rx.end(); ++it) { - auto& pck = *it; - pj_pool_reset(rdata_.tp_info.pool); - pj_gettimeofday(&rdata_.pkt_info.timestamp); - rdata_.pkt_info.len = pck.size(); - std::copy_n(pck.data(), pck.size(), rdata_.pkt_info.packet); - auto eaten = pjsip_tpmgr_receive_packet(trData_.base.tpmgr, &rdata_); - if (eaten != rdata_.pkt_info.len) { - // partial sip packet received - auto npck_it = std::next(it); - if (npck_it != rx.end()) { - // drop current packet, merge reminder with next one - auto& npck = *npck_it; - npck.insert(npck.begin(), pck.begin()+eaten, pck.end()); - } else { - // erase eaten part, keep reminder - pck.erase(pck.begin(), pck.begin()+eaten); - { - std::lock_guard<std::mutex> l(rxMtx_); - rxPending_.splice(rxPending_.begin(), rx, it); - } - break; - } - } - } - putBuff(std::move(rx)); - rxCv_.notify_all(); - - // Report status GnuTLS -> SIP transport - decltype(outputAckBuff_) ackBuf; - { - std::lock_guard<std::mutex> l(outputBuffMtx_); - ackBuf = std::move(outputAckBuff_); - } - for (const auto& pair: ackBuf) { - const auto& f = pair.first; - f.tdata_op_key->tdata = nullptr; - RING_DBG("status: %ld", pair.second); - if (f.tdata_op_key->callback) - f.tdata_op_key->callback(getTransportBase(), f.tdata_op_key->token, - pair.second); - } - cv_.notify_all(); -} - -bool -SipsIceTransport::setup() +SipsIceTransport::certGetCn(const pj_str_t* gen_name, pj_str_t* cn) { - RING_DBG("Starting GnuTLS thread"); - - // permit incoming packets - ice_->setOnRecv(comp_id_, [this](uint8_t* buf, size_t len) { - { - std::lock_guard<std::mutex> l(inputBuffMtx_); - tlsInputBuff_.emplace_back(buf, buf+len); - canRead_ = true; - RING_DBG("TLS(ice): rx %zuB", len); - } - cv_.notify_all(); - return len; - }); - - if (is_server_) { - gnutls_key_generate(&cookie_key_, GNUTLS_COOKIE_KEY_SIZE); - state_ = TlsConnectionState::COOKIE; - return true; - } + pj_str_t CN_sign = {(char*)"CN=", 3}; + char *p, *q; - return startTlsSession() == PJ_SUCCESS; -} + std::memset(cn, 0, sizeof(*cn)); -void -SipsIceTransport::loop() -{ - if (not ice_->isRunning()) { - shutdown(); + p = pj_strstr(gen_name, &CN_sign); + if (!p) return; - } - - if (state_ == TlsConnectionState::COOKIE) { - { - std::unique_lock<std::mutex> l(inputBuffMtx_); - if (tlsInputBuff_.empty()) { - cv_.wait(l, [&](){ - return state_ != TlsConnectionState::COOKIE or not tlsInputBuff_.empty(); - }); - } - if (state_ != TlsConnectionState::COOKIE) - return; - const auto& pck = tlsInputBuff_.front(); - std::memset(&prestate_, 0, sizeof(prestate_)); - int ret = gnutls_dtls_cookie_verify(&cookie_key_, - &trData_.base.key.rem_addr, - trData_.base.addr_len, - (char*)pck.data(), pck.size(), - &prestate_); - if (ret < 0) { - gnutls_dtls_cookie_send(&cookie_key_, - &trData_.base.key.rem_addr, - trData_.base.addr_len, &prestate_, this, - [](gnutls_transport_ptr_t t, - const void* d, size_t s) -> ssize_t { - auto this_ = reinterpret_cast<SipsIceTransport*>(t); - return this_->tlsSend(d, s); - }); - tlsInputBuff_.pop_front(); - if (tlsInputBuff_.empty()) - canRead_ = false; - return; - } - } - startTlsSession(); - } - - if (state_ == TlsConnectionState::HANDSHAKING) { - if (clock::now() - handshakeStart_ > param_.timeout) { - onHandshakeComplete(PJ_ETIMEDOUT); - return; - } - auto status = tryHandshake(); - if (status != PJ_EPENDING) - onHandshakeComplete(status); - } - - if (state_ == TlsConnectionState::ESTABLISHED) { - { - std::mutex flagsMtx_ {}; - std::unique_lock<std::mutex> l(flagsMtx_); - cv_.wait(l, [&](){ - return state_ != TlsConnectionState::ESTABLISHED or canRead_ or canWrite_; - }); - } - - if (state_ != TlsConnectionState::ESTABLISHED and not getTransportBase()->is_shutdown) - return; - - decltype(rxPending_) rx; - while (canRead_ or gnutls_record_check_pending(session_)) { - if (rx.empty()) - getBuff(rx, PJSIP_MAX_PKT_LEN); - auto& buf = rx.front(); - const auto decrypted_size = gnutls_record_recv(session_, buf.data(), buf.size()); - - if (decrypted_size > 0/* || transport error */) { - buf.resize(decrypted_size); - { - std::lock_guard<std::mutex> l(rxMtx_); - rxPending_.splice(rxPending_.end(), rx, rx.begin()); - } - } else if (decrypted_size == 0) { - /* EOF */ - tlsThread_.stop(); - break; - } else if (decrypted_size == GNUTLS_E_AGAIN or - decrypted_size == GNUTLS_E_INTERRUPTED) { - break; - } else if (decrypted_size == GNUTLS_E_REHANDSHAKE) { - /* Seems like we are renegotiating */ - - RING_DBG("rehandshake"); - auto try_handshake_status = tryHandshake(); - - /* Not pending is either success or failed */ - if (try_handshake_status != PJ_EPENDING) { - if (!onHandshakeComplete(try_handshake_status)) { - break; - } - } - - if (try_handshake_status != PJ_SUCCESS and - try_handshake_status != PJ_EPENDING) { - break; - } - } else if (!gnutls_error_is_fatal(decrypted_size)) { - /* non-fatal error, let's just continue */ - } else { - shutdown(); - break; - } - } - putBuff(std::move(rx)); - flushOutputBuff(); - } -} - -void -SipsIceTransport::clean() -{ - RING_DBG("Ending GnuTLS thread"); - - // Forbid GnuTLS <-> ICE IOs - ice_->setOnRecv(comp_id_, nullptr); - tlsInputBuff_.clear(); - canRead_ = false; - canWrite_ = false; - - { - // Reply all SIP transport send requests with ENOTCONN status error - std::unique_lock<std::mutex> l(outputBuffMtx_); - - for (const auto& f : outputBuff_) { - f.tdata_op_key->tdata = nullptr; - if (f.tdata_op_key->callback) - outputAckBuff_.emplace_back(std::move(f), -PJ_RETURN_OS_ERROR(OSERR_ENOTCONN)); // see handleEvents() - } - outputBuff_.clear(); - - // make sure that all callbacks are called before closing - cv_.wait(l, [&](){ - return outputAckBuff_.empty(); - }); - } - - // note: incoming packets (rxPending_) will be processed in destructor - - if (cookie_key_.data) { - gnutls_free(cookie_key_.data); - cookie_key_.data = nullptr; - cookie_key_.size = 0; - } - - closeTlsSession(); -} - -IpAddr -SipsIceTransport::getLocalAddress() const -{ - return ice_->getLocalAddress(comp_id_); -} - -IpAddr -SipsIceTransport::getRemoteAddress() const -{ - return ice_->getRemoteAddress(comp_id_); -} - -ssize_t -SipsIceTransport::tlsSend(const void* d, size_t s) -{ - return ice_->send(comp_id_, (const uint8_t*)d, s); -} -ssize_t -SipsIceTransport::tlsRecv(void* d , size_t s) -{ - std::lock_guard<std::mutex> l(inputBuffMtx_); - if (tlsInputBuff_.empty()) { - gnutls_transport_set_errno(session_, EAGAIN); - return -1; - } - const auto& front = tlsInputBuff_.front(); - const auto n = std::min(front.size(), s); - std::copy_n(front.begin(), n, (uint8_t*)d); - tlsInputBuff_.pop_front(); - if (tlsInputBuff_.empty()) - canRead_ = false; - return n; -} - -int -SipsIceTransport::waitForTlsData(unsigned ms) -{ - std::unique_lock<std::mutex> l(inputBuffMtx_); - if (tlsInputBuff_.empty()) { - cv_.wait_for(l, std::chrono::milliseconds(ms), [&]() { - return state_ == TlsConnectionState::DISCONNECTED or not tlsInputBuff_.empty(); - }); - } - if (state_ == TlsConnectionState::DISCONNECTED) { - gnutls_transport_set_errno(session_, EINTR); - return -1; - } - return tlsInputBuff_.empty() ? 0 : tlsInputBuff_.front().size(); + p += 3; /* shift pointer to value part */ + pj_strset(cn, p, gen_name->slen - (p - gen_name->ptr)); + q = pj_strchr(cn, ','); + if (q) + cn->slen = q - p; } pj_status_t @@ -966,251 +631,30 @@ SipsIceTransport::send(pjsip_tx_data *tdata, const pj_sockaddr_t *rem_addr, addr_len==sizeof(pj_sockaddr_in6)), PJ_EINVAL); + tdata->op_key.tdata = tdata; tdata->op_key.token = token; tdata->op_key.callback = callback; - if (state_ == TlsConnectionState::ESTABLISHED) { - decltype(txBuff_) tx; - size_t size = tdata->buf.cur - tdata->buf.start; - getBuff(tx, (uint8_t*)tdata->buf.start, (uint8_t*)tdata->buf.cur); - { - std::lock_guard<std::mutex> l(outputBuffMtx_); - txBuff_.splice(txBuff_.end(), std::move(tx)); - } - tdata->op_key.tdata = nullptr; - if (tdata->op_key.callback) - tdata->op_key.callback(getTransportBase(), token, size); - } else { - std::lock_guard<std::mutex> l(outputBuffMtx_); - tdata->op_key.tdata = tdata; - outputBuff_.emplace_back(DelayedTxData{&tdata->op_key, {}}); - if (tdata->msg && tdata->msg->type == PJSIP_REQUEST_MSG) { - auto& dtd = outputBuff_.back(); - dtd.timeout = clock::now(); - dtd.timeout += std::chrono::milliseconds(pjsip_cfg()->tsx.td); - } - } - canWrite_ = true; - cv_.notify_all(); - return PJ_EPENDING; -} - -pj_status_t -SipsIceTransport::flushOutputBuff() -{ - ssize_t status = PJ_SUCCESS; - - // send delayed data first - for (;;) { - bool fatal = true; - DelayedTxData f; - { - std::lock_guard<std::mutex> l(outputBuffMtx_); - if (outputBuff_.empty()) - break; - f = std::move(outputBuff_.front()); - outputBuff_.pop_front(); - } - - // Too late? - if (f.timeout != clock::time_point() && f.timeout < clock::now()) { - status = GNUTLS_E_TIMEDOUT; - fatal = false; - } else { - status = trySend(f.tdata_op_key); - fatal = status < 0; - if (fatal) { - if (gnutls_error_is_fatal(status)) - tlsThread_.stop(); - else { - // Failed but non-fatal. Retry later. - std::lock_guard<std::mutex> l(outputBuffMtx_); - outputBuff_.emplace_front(std::move(f)); - } - } - } - - f.tdata_op_key->tdata = nullptr; - - if (f.tdata_op_key->callback) { - std::lock_guard<std::mutex> l(outputBuffMtx_); - outputAckBuff_.emplace_back(std::move(f), status > 0 ? status : -tls_status_from_err(status)); // see handleEvents() - } - - if (fatal) - return -tls_status_from_err(status); - } - - decltype(txBuff_) tx; - { - std::lock_guard<std::mutex> l(outputBuffMtx_); - tx = std::move(txBuff_); - canWrite_ = false; - } - for (auto it = tx.begin(); it != tx.end(); ++it) { - const auto& msg = *it; - const auto nwritten = gnutls_record_send(session_, msg.data(), msg.size()); - if (nwritten <= 0) { - status = nwritten; - if (gnutls_error_is_fatal(status)) - tlsThread_.stop(); - { - std::lock_guard<std::mutex> l(outputBuffMtx_); - txBuff_.splice(txBuff_.begin(), tx, it, tx.end()); - canWrite_ = true; - } - break; - } - } - putBuff(std::move(tx)); - return status > 0 ? PJ_SUCCESS : -tls_status_from_err(status); -} - -ssize_t -SipsIceTransport::trySend(pjsip_tx_data_op_key *pck) -{ - const auto tdata = pck->tdata; - const size_t size = tdata->buf.cur - tdata->buf.start; - const size_t max_tx_sz = gnutls_dtls_get_data_mtu(session_); - - size_t total_written = 0; - while (total_written < size) { - /* Ask GnuTLS to encrypt our plaintext now. GnuTLS will use the push - * callback to actually send the encrypted bytes. */ - const auto tx_size = std::min(max_tx_sz, size - total_written); - const auto nwritten = gnutls_record_send(session_, - tdata->buf.start + total_written, - tx_size); - if (nwritten <= 0) { - /* Normally we would have to retry record_send but our internal - * state has not changed, so we have to ask for more data first. - * We will just try again later, although this should never happen. - */ - return nwritten; - } - - /* Good, some data was encrypted and written */ - total_written += nwritten; - } - return total_written; -} - -void -SipsIceTransport::shutdown() -{ - RING_DBG("%s", __PRETTY_FUNCTION__); - state_ = TlsConnectionState::DISCONNECTED; - tlsThread_.stop(); - cv_.notify_all(); -} - -void -SipsIceTransport::getBuff(decltype(buffPool_)& l, const uint8_t* b, const uint8_t* e) -{ - std::lock_guard<std::mutex> lk(buffPoolMtx_); - if (buffPool_.empty()) - l.emplace_back(b, e); - else { - l.splice(l.end(), buffPool_, buffPool_.begin()); - auto& buf = l.back(); - buf.resize(std::distance(b, e)); - std::copy(b, e, buf.begin()); - } -} - -void -SipsIceTransport::getBuff(decltype(buffPool_)& l, const size_t s) -{ - std::lock_guard<std::mutex> lk(buffPoolMtx_); - if (buffPool_.empty()) - l.emplace_back(s); - else { - l.splice(l.end(), buffPool_, buffPool_.begin()); - auto& buf = l.back(); - buf.resize(s); - } -} - -void -SipsIceTransport::putBuff(decltype(buffPool_)&& l) -{ - std::lock_guard<std::mutex> lk(buffPoolMtx_); - buffPool_.splice(buffPool_.end(), l); -} - -pj_status_t -SipsIceTransport::tls_status_from_err(int err) -{ - pj_status_t status; + // Asynchronous send + const std::size_t size = tdata->buf.cur - tdata->buf.start; + auto ret = tls_->async_send(tdata->buf.start, size, [=](std::size_t bytes_sent){ + if (bytes_sent == 0) + bytes_sent = -PJ_RETURN_OS_ERROR(OSERR_ENOTCONN); + std::lock_guard<std::mutex> l(txAckQueueMutex_); + txAckQueue_.emplace_back(std::make_pair(tdata, bytes_sent)); + }); - switch (err) { - case GNUTLS_E_SUCCESS: - status = PJ_SUCCESS; - break; - case GNUTLS_E_MEMORY_ERROR: - status = PJ_ENOMEM; - break; - case GNUTLS_E_LARGE_PACKET: - status = PJ_ETOOBIG; - break; - case GNUTLS_E_NO_CERTIFICATE_FOUND: - status = PJ_ENOTFOUND; - break; - case GNUTLS_E_SESSION_EOF: - status = PJ_EEOF; - break; - case GNUTLS_E_HANDSHAKE_TOO_LARGE: - status = PJ_ETOOBIG; - break; - case GNUTLS_E_EXPIRED: - status = PJ_EGONE; - break; - case GNUTLS_E_TIMEDOUT: - status = PJ_ETIMEDOUT; - break; - case GNUTLS_E_PREMATURE_TERMINATION: - status = PJ_ECANCELLED; - break; - case GNUTLS_E_INTERNAL_ERROR: - case GNUTLS_E_UNIMPLEMENTED_FEATURE: - status = PJ_EBUG; - break; - case GNUTLS_E_AGAIN: - case GNUTLS_E_INTERRUPTED: - case GNUTLS_E_REHANDSHAKE: - status = PJ_EPENDING; - break; - case GNUTLS_E_TOO_MANY_EMPTY_PACKETS: - case GNUTLS_E_TOO_MANY_HANDSHAKE_PACKETS: - case GNUTLS_E_RECORD_LIMIT_REACHED: - status = PJ_ETOOMANY; - break; - case GNUTLS_E_UNSUPPORTED_VERSION_PACKET: - case GNUTLS_E_UNSUPPORTED_SIGNATURE_ALGORITHM: - case GNUTLS_E_UNSUPPORTED_CERTIFICATE_TYPE: - case GNUTLS_E_X509_UNSUPPORTED_ATTRIBUTE: - case GNUTLS_E_X509_UNSUPPORTED_EXTENSION: - case GNUTLS_E_X509_UNSUPPORTED_CRITICAL_EXTENSION: - status = PJ_ENOTSUP; - break; - case GNUTLS_E_INVALID_SESSION: - case GNUTLS_E_INVALID_REQUEST: - case GNUTLS_E_INVALID_PASSWORD: - case GNUTLS_E_ILLEGAL_PARAMETER: - case GNUTLS_E_RECEIVED_ILLEGAL_EXTENSION: - case GNUTLS_E_UNEXPECTED_PACKET: - case GNUTLS_E_UNEXPECTED_PACKET_LENGTH: - case GNUTLS_E_UNEXPECTED_HANDSHAKE_PACKET: - case GNUTLS_E_UNWANTED_ALGORITHM: - case GNUTLS_E_USER_ERROR: - status = PJ_EINVAL; - break; - default: - status = PJ_EUNKNOWN; - break; + // Shutdown on fatal errors + if (gnutls_error_is_fatal(ret)) { + tdata->op_key.tdata = nullptr; + if (callback) + callback(&trData_.base, token, -PJ_RETURN_OS_ERROR(OSERR_ENOTCONN)); + RING_ERR("[TLS] send failed: %s", gnutls_strerror(ret)); + tls_->shutdown(); + return tls_status_from_err(ret); } - return status; + return PJ_EPENDING; } }} // namespace ring::tls diff --git a/src/ringdht/sips_transport_ice.h b/src/ringdht/sips_transport_ice.h index 365d89735151bd924f148e35675b22dc2470bc74..27b1d65a513a3a6051300e4300fd72b829d89b07 100644 --- a/src/ringdht/sips_transport_ice.h +++ b/src/ringdht/sips_transport_ice.h @@ -1,7 +1,8 @@ /* - * Copyright (C) 2004-2015 Savoir-faire Linux Inc. + * Copyright (C) 2004-2016 Savoir-faire Linux Inc. * * Author: Adrien Béraud <adrien.beraud@savoirfairelinux.com> + * Author: Guillaume Roguez <guillaume.roguez@savoirfairelinux.com> * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -20,10 +21,9 @@ #pragma once +#include "security/tls_session.h" #include "ip_utils.h" -#include "threadloop.h" - -#include <opendht/crypto.h> +#include "noncopyable.h" #include <pjsip.h> #include <pj/pool.h> @@ -36,11 +36,10 @@ #include <functional> #include <memory> #include <mutex> -#include <condition_variable> #include <chrono> -#include <future> #include <queue> #include <utility> +#include <vector> namespace ring { class IceTransport; @@ -48,23 +47,11 @@ class IceTransport; namespace ring { namespace tls { -enum class TlsConnectionState { - DISCONNECTED, - COOKIE, - HANDSHAKING, - ESTABLISHED -}; - -struct TlsParams { - std::string ca_list; - dht::crypto::Identity id; - std::shared_future<std::unique_ptr<gnutls_dh_params_int, decltype(gnutls_dh_params_deinit)&>> dh_params; - std::chrono::steady_clock::duration timeout; - std::function<pj_status_t(unsigned status, - const gnutls_datum_t* cert_list, - unsigned cert_list_size)> cert_check; -}; - +/** + * SipsIceTransport + * + * Implements TLS transport as an pjsip_transport + */ struct SipsIceTransport { using clock = std::chrono::steady_clock; @@ -81,57 +68,36 @@ struct SipsIceTransport void shutdown(); - IpAddr getLocalAddress() const; - IpAddr getRemoteAddress() const; - - std::shared_ptr<IceTransport> getIceTransport() const { - return ice_; - } + std::shared_ptr<IceTransport> getIceTransport() const { return ice_; } + pjsip_transport* getTransportBase() { return &trData_.base; } - pjsip_transport* getTransportBase() { - return &trData_.base; - } + IpAddr getLocalAddress() const { return local_; } + IpAddr getRemoteAddress() const { return remote_; } private: - std::unique_ptr<pj_pool_t, decltype(pj_pool_release)&> pool_; - std::unique_ptr<pj_pool_t, decltype(pj_pool_release)&> rxPool_; - - using DelayedTxData = struct { - pjsip_tx_data_op_key *tdata_op_key; - clock::time_point timeout; - }; + NON_COPYABLE(SipsIceTransport); - TransportData trData_; // uplink to this (used in C callbacks) const std::shared_ptr<IceTransport> ice_; const int comp_id_; - - TlsParams param_; - - bool is_server_ {false}; - //bool has_pending_connect_ {false}; + const std::function<int(unsigned, const gnutls_datum_t*, unsigned)> certCheck_; IpAddr local_ {}; IpAddr remote_ {}; - ThreadLoop tlsThread_; - std::condition_variable_any cv_; - std::atomic<bool> canRead_ {false}; - std::atomic<bool> canWrite_ {false}; + // PJSIP transport backend + + TransportData trData_; // uplink to "this" (used by PJSIP called C-callbacks) - // TODO - pj_status_t verifyStatus_ {}; - int last_err_; + std::unique_ptr<pj_pool_t, decltype(pj_pool_release)*> pool_; + std::unique_ptr<pj_pool_t, decltype(pj_pool_release)*> rxPool_; + + pjsip_rx_data rdata_; pj_ssl_cert_info localCertInfo_; pj_ssl_cert_info remoteCertInfo_; - std::atomic<TlsConnectionState> state_ {TlsConnectionState::DISCONNECTED}; - clock::time_point handshakeStart_; + pj_status_t verifyStatus_ {PJ_EUNKNOWN}; - gnutls_session_t session_ {nullptr}; - std::unique_ptr<gnutls_certificate_credentials_st, decltype(gnutls_certificate_free_credentials)&> xcred_; - gnutls_priority_t priority_cache_ {nullptr}; - gnutls_datum_t cookie_key_ {nullptr, 0}; - gnutls_dtls_prestate_st prestate_; + // TlsSession backend struct ChangeStateEventData { pj_ssl_sock_info ssl_info; @@ -140,60 +106,29 @@ private: decltype(PJSIP_TP_STATE_DISCONNECTED) state; }; - std::mutex stateChangeEventsMutex_; - std::queue<ChangeStateEventData> stateChangeEvents_; + std::unique_ptr<TlsSession> tls_; + std::atomic_bool tlsConnected_ {false}; // set by updateTransportState - /** - * To be called on a regular basis to receive packets - */ - void handleEvents(); - - // ThreadLoop - bool setup(); - void loop(); - void clean(); - - // SIP transport <-> GnuTLS - pj_status_t send(pjsip_tx_data* tdata, const pj_sockaddr_t* rem_addr, - int addr_len, void* token, - pjsip_transport_callback callback); - ssize_t trySend(pjsip_tx_data_op_key* tdata); - pj_status_t flushOutputBuff(); - std::list<DelayedTxData> outputBuff_; - std::list<std::pair<DelayedTxData, ssize_t>> outputAckBuff_; // acknowledged outputBuff_ - std::list<std::vector<uint8_t>> txBuff_; - std::mutex outputBuffMtx_; + std::mutex stateChangeEventsMutex_ {}; + std::list<ChangeStateEventData> stateChangeEvents_ {}; std::mutex rxMtx_; - std::condition_variable_any rxCv_; std::list<std::vector<uint8_t>> rxPending_; - pjsip_rx_data rdata_; - // data buffer pool - std::list<std::vector<uint8_t>> buffPool_; - std::mutex buffPoolMtx_; - void getBuff(decltype(buffPool_)& l, const uint8_t* b, const uint8_t* e); - void getBuff(decltype(buffPool_)& l, const size_t s); - void putBuff(decltype(buffPool_)&& l); - - // GnuTLS <-> ICE - ssize_t tlsSend(const void*, size_t); - ssize_t tlsRecv(void* d , size_t s); - int waitForTlsData(unsigned ms); - std::mutex inputBuffMtx_; - std::list<std::vector<uint8_t>> tlsInputBuff_; - - pj_status_t startTlsSession(); - void closeTlsSession(); - - pj_status_t tryHandshake(); - void certGetCn(const pj_str_t* gen_name, pj_str_t* cn); - void certGetInfo(pj_pool_t* pool, pj_ssl_cert_info* ci, const gnutls_datum_t* cert, size_t crt_raw_num); - void certUpdate(); - bool onHandshakeComplete(pj_status_t status); - int verifyCertificate(); - void getInfo(pj_ssl_sock_info* info); - static pj_status_t tls_status_from_err(int err); + std::mutex txAckQueueMutex_; + std::list<std::pair<pjsip_tx_data*, ssize_t>> txAckQueue_; + + pj_status_t send(pjsip_tx_data*, const pj_sockaddr_t*, int, void*, pjsip_transport_callback); + void handleEvents(); + void pushChangeStateEvent(ChangeStateEventData&&); + void updateTransportState(pjsip_transport_state); + void certGetInfo(pj_pool_t*, pj_ssl_cert_info*, const gnutls_datum_t*, size_t); + void certGetCn(const pj_str_t*, pj_str_t*); + void getInfo(pj_ssl_sock_info*, bool); + void onTlsStateChange(TlsSessionState); + void onRxData(std::vector<uint8_t>&&); + void onCertificatesUpdate(const gnutls_datum_t*, const gnutls_datum_t*, unsigned int); + int verifyCertificate(gnutls_session_t); }; }} // namespace ring::tls diff --git a/src/security/Makefile.am b/src/security/Makefile.am index 44f1b26bae7613135e6ba9c5a4b96e3e5506fad7..8321a993b0313a2f1bf8b85dc5964ebfc54e5829 100644 --- a/src/security/Makefile.am +++ b/src/security/Makefile.am @@ -4,7 +4,9 @@ noinst_LTLIBRARIES = libsecurity.la libsecurity_la_CXXFLAGS = @CXXFLAGS@ -I$(top_srcdir)/src libsecurity_la_SOURCES = \ - tlsvalidator.cpp \ - tlsvalidator.h \ - certstore.cpp \ - certstore.h + tls_session.cpp \ + tls_session.h \ + tlsvalidator.cpp \ + tlsvalidator.h \ + certstore.cpp \ + certstore.h diff --git a/src/security/tls_session.cpp b/src/security/tls_session.cpp new file mode 100644 index 0000000000000000000000000000000000000000..b6f0d5fa73917ff70d0d728ecc9861410cb6f174 --- /dev/null +++ b/src/security/tls_session.cpp @@ -0,0 +1,662 @@ +/* + * Copyright (C) 2004-2016 Savoir-faire Linux Inc. + * + * Author: Adrien Béraud <adrien.beraud@savoirfairelinux.com> + * Author: Guillaume Roguez <guillaume.roguez@savoirfairelinux.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 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 General Public License for more details. + * + * You should have received a copy of the GNU 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. + */ + +#include <ip_utils.h> // DO NOT CHANGE ORDER OF THIS INCLUDE +#include <opendht/crypto.h> // OR MINGWIN FAILS TO BUILD + +#include "tls_session.h" + +#include "ice_socket.h" +#include "ice_transport.h" +#include "logger.h" +#include "noncopyable.h" +#include "intrin.h" + +#include <gnutls/dtls.h> +#include <gnutls/abstract.h> + +#include <algorithm> +#include <cstring> // std::memset + +namespace ring { namespace tls { + +static constexpr int DTLS_MTU {1400}; // limit for networks like ADSL +static constexpr const char* TLS_PRIORITY_STRING {"SECURE192:-VERS-TLS-ALL:+VERS-DTLS-ALL:%SERVER_PRECEDENCE"}; +static constexpr ssize_t FLOOD_THRESHOLD {4*1024}; +static constexpr auto FLOOD_PAUSE = std::chrono::milliseconds(100); // Time to wait after an invalid cookie packet (anti flood attack) +static constexpr std::size_t INPUT_MAX_SIZE {1000}; // Maximum packet to store before dropping (pkt size = DTLS_MTU) + +class TlsSession::TlsCertificateCredendials +{ + using T = gnutls_certificate_credentials_t; +public: + TlsCertificateCredendials() { + int ret = gnutls_certificate_allocate_credentials(&creds_); + if (ret < 0) { + RING_ERR("gnutls_certificate_allocate_credentials() failed with ret=%d", ret); + throw std::bad_alloc(); + } + } + + ~TlsCertificateCredendials() { + gnutls_certificate_free_credentials(creds_); + } + + operator T() { return creds_; } + +private: + NON_COPYABLE(TlsCertificateCredendials); + T creds_; +}; + +TlsSession::TlsSession(std::shared_ptr<IceTransport> ice, int ice_comp_id, + const TlsParams& params, const TlsSessionCallbacks& cbs) + : socket_(new IceSocket(ice, ice_comp_id)) + , isServer_(not ice->isInitiator()) + , params_(params) + , callbacks_(cbs) + , xcred_(nullptr) + , thread_([this] { return setup(); }, + [this] { process(); }, + [this] { cleanup(); }) +{ + // Setup TLS algorithms priority list + auto ret = gnutls_priority_init(&priority_cache_, TLS_PRIORITY_STRING, nullptr); + if (ret != GNUTLS_E_SUCCESS) { + RING_ERR("[TLS] priority setup failed: %s", gnutls_strerror(ret)); + throw std::runtime_error("TlsSession"); + } + + socket_->setOnRecv([this](uint8_t* buf, size_t len) { + std::lock_guard<std::mutex> lk {ioMutex_}; + if (rxQueue_.size() == INPUT_MAX_SIZE) { + rxQueue_.pop_front(); // drop oldest packet if input buffer is full + ++stRxRawPacketDropCnt_; + } + rxQueue_.emplace_back(buf, buf+len); + ++stRxRawPacketCnt_; + stRxRawBytesCnt_ += len; + ioCv_.notify_one(); + return len; + }); + + // Run FSM into dedicated thread + thread_.start(); +} + +TlsSession::~TlsSession() +{ + shutdown(); + thread_.join(); + + socket_->setOnRecv(nullptr); + + if (priority_cache_) + gnutls_priority_deinit(priority_cache_); +} + +const char* +TlsSession::typeName() const +{ + return isServer_ ? "server" : "client"; +} + +void +TlsSession::dump_io_stats() const +{ + RING_WARN("[TLS] RxRawPckt=%zu (%zu bytes)", stRxRawPacketCnt_, stRxRawBytesCnt_); +} + +TlsSessionState +TlsSession::setupClient() +{ + auto ret = gnutls_init(&session_, GNUTLS_CLIENT | GNUTLS_DATAGRAM); + if (ret != GNUTLS_E_SUCCESS) { + RING_ERR("[TLS] session init failed: %s", gnutls_strerror(ret)); + return TlsSessionState::SHUTDOWN; + } + + if (not commonSessionInit()) { + return TlsSessionState::SHUTDOWN; + } + + return TlsSessionState::HANDSHAKE; +} + +TlsSessionState +TlsSession::setupServer() +{ + gnutls_key_generate(&cookie_key_, GNUTLS_COOKIE_KEY_SIZE); + return TlsSessionState::COOKIE; +} + +void +TlsSession::initCredentials() +{ + int ret; + + // credentials for handshaking and transmission + xcred_.reset(new TlsCertificateCredendials()); + + if (callbacks_.verifyCertificate) + gnutls_certificate_set_verify_function(*xcred_, [](gnutls_session_t session) -> int { + auto this_ = reinterpret_cast<TlsSession*>(gnutls_session_get_ptr(session)); + return this_->callbacks_.verifyCertificate(session); + }); + + // Load user-given CA list + if (not params_.ca_list.empty()) { + // Try PEM format first + ret = gnutls_certificate_set_x509_trust_file(*xcred_, params_.ca_list.c_str(), + GNUTLS_X509_FMT_PEM); + + // Then DER format + if (ret < 0) + ret = gnutls_certificate_set_x509_trust_file(*xcred_, params_.ca_list.c_str(), + GNUTLS_X509_FMT_DER); + if (ret < 0) + throw std::runtime_error("can't load CA " + params_.ca_list + ": " + + std::string(gnutls_strerror(ret))); + + RING_DBG("[TLS] CA list %s loadev", params_.ca_list.c_str()); + } + + // Load user-given identity (key and passwd) + if (params_.cert) { + ret = gnutls_certificate_set_x509_key(*xcred_, ¶ms_.cert->cert, 1, + params_.cert_key->x509_key); + if (ret < 0) + throw std::runtime_error("can't load certificate: " + + std::string(gnutls_strerror(ret))); + + RING_DBG("[TLS] User identity loaded"); + } + + // Setup DH-params (server only, may block on dh_params.get()) + if (isServer_) { + if (auto& dh_params = params_.dh_params.get()) + gnutls_certificate_set_dh_params(*xcred_, dh_params.get()); + else + RING_WARN("[TLS] DH params unavailable"); // YOMGUI: need to stop? + } +} + +bool +TlsSession::commonSessionInit() +{ + int ret; + + ret = gnutls_priority_set(session_, priority_cache_); + if (ret != GNUTLS_E_SUCCESS) { + RING_ERR("[TLS] session TLS priority set failed: %s", gnutls_strerror(ret)); + return false; + } + + ret = gnutls_credentials_set(session_, GNUTLS_CRD_CERTIFICATE, *xcred_); + if (ret != GNUTLS_E_SUCCESS) { + RING_ERR("[TLS] session credential set failed: %s", gnutls_strerror(ret)); + return false; + } + + // Stuff for transport callbacks + gnutls_session_set_ptr(session_, this); + gnutls_transport_set_ptr(session_, this); + gnutls_dtls_set_mtu(session_, DTLS_MTU); + + // TODO: minimize user timeout + auto timeout = std::chrono::duration_cast<std::chrono::milliseconds>(params_.timeout).count(); + gnutls_dtls_set_timeouts(session_, 1000, timeout); + + gnutls_transport_set_vec_push_function(session_, + [](gnutls_transport_ptr_t t, const giovec_t* iov, + int iovcnt) -> ssize_t { + auto this_ = reinterpret_cast<TlsSession*>(t); + return this_->sendRawVec(iov, iovcnt); + }); + gnutls_transport_set_pull_function(session_, + [](gnutls_transport_ptr_t t, void* d, size_t s) -> ssize_t { + auto this_ = reinterpret_cast<TlsSession*>(t); + return this_->recvRaw(d, s); + }); + gnutls_transport_set_pull_timeout_function(session_, + [](gnutls_transport_ptr_t t, unsigned ms) -> int { + auto this_ = reinterpret_cast<TlsSession*>(t); + return this_->waitForRawData(ms); + }); + + return true; +} + +// Called by anyone to stop the connection and the FSM thread +void +TlsSession::shutdown() +{ + state_ = TlsSessionState::SHUTDOWN; + ioCv_.notify_one(); // unblock waiting FSM +} + +const char* +TlsSession::getCurrentCipherSuiteId(std::array<uint8_t, 2>& cs_id) const +{ + auto cipher = gnutls_cipher_get(session_); + gnutls_cipher_algorithm_t lookup; + + // Loop on ciphers suite until our cipher is found + for (std::size_t i=0; ; ++i) { + const char* const suite = gnutls_cipher_suite_info(i, cs_id.data(), nullptr, &lookup, nullptr, nullptr); + if (lookup == cipher) + return suite; + } + + return {}; +} + +// Called by application to send data to encrypt. +ssize_t +TlsSession::async_send(void* data, std::size_t size, TxDataCompleteFunc on_send_complete) +{ + if (state_ == TlsSessionState::SHUTDOWN) + return GNUTLS_E_INVALID_SESSION; + std::lock_guard<std::mutex> lk {ioMutex_}; + txQueue_.emplace_back(TxData {data, size, on_send_complete}); + ioCv_.notify_one(); + return GNUTLS_E_SUCCESS; +} + +ssize_t +TlsSession::send(const TxData& tx_data) +{ + std::size_t max_tx_sz = gnutls_dtls_get_data_mtu(session_); + std::size_t tx_size = tx_data.size; + auto ptr = static_cast<uint8_t*>(tx_data.ptr); + + // Split user data into MTU-suitable chunck + size_t total_written = 0; + while (total_written < tx_size) { + auto chunck_sz = std::min(max_tx_sz, tx_size - total_written); + auto nwritten = gnutls_record_send(session_, ptr + total_written, chunck_sz); + if (nwritten <= 0) { + /* Normally we would have to retry record_send but our internal + * state has not changed, so we have to ask for more data first. + * We will just try again later, although this should never happen. + */ + RING_WARN("[TLS] send failed (only %zu bytes sent): %s", total_written, + gnutls_strerror(nwritten)); + return nwritten; + } + + total_written += nwritten; + } + return total_written; +} + +// Called by GNUTLS to send encrypted packet to low-level transport. +// Should return a positive number indicating the bytes sent, and -1 on error. +ssize_t +TlsSession::sendRaw(const void* buf, size_t size) +{ + auto ret = socket_->send(reinterpret_cast<const unsigned char*>(buf), size); + if (ret > 0) { + // log only on success + ++stTxRawPacketCnt_; + stTxRawBytesCnt_ += size; + } + return ret; +} + +// Called by GNUTLS to send encrypted packet to low-level transport. +// Should return a positive number indicating the bytes sent, and -1 on error. +ssize_t +TlsSession::sendRawVec(const giovec_t* iov, int iovcnt) +{ + ssize_t sent = 0; + for (int i=0; i<iovcnt; ++i) { + const giovec_t& dat = iov[i]; + ssize_t ret = sendRaw(dat.iov_base, dat.iov_len); + if (ret < 0) + return ret; + sent += ret; + } + return sent; +} + +// Called by GNUTLS to receive encrypted packet from low-level transport. +// Should return 0 on connection termination, +// a positive number indicating the number of bytes received, +// and -1 on error. +ssize_t +TlsSession::recvRaw(void* buf, size_t size) +{ + std::lock_guard<std::mutex> lk {ioMutex_}; + if (rxQueue_.empty()) { + gnutls_transport_set_errno(session_, EAGAIN); + return -1; + } + + const auto& pkt = rxQueue_.front(); + const std::size_t count = std::min(pkt.size(), size); + std::copy_n(pkt.begin(), count, reinterpret_cast<uint8_t*>(buf)); + rxQueue_.pop_front(); + return count; +} + +// Called by GNUTLS to wait for encrypted packet from low-level transport. +// 'timeout' is in milliseconds. +// Should return 0 on connection termination, +// a positive number indicating the number of bytes received, +// and -1 on error. +int +TlsSession::waitForRawData(unsigned timeout) +{ + std::unique_lock<std::mutex> lk {ioMutex_}; + if (not ioCv_.wait_for(lk, std::chrono::milliseconds(timeout), + [this]{ return !rxQueue_.empty() or state_ == TlsSessionState::SHUTDOWN; })) + return 0; + + // shutdown? + if (state_ == TlsSessionState::SHUTDOWN) { + gnutls_transport_set_errno(session_, EINTR); + return -1; + } + + return rxQueue_.front().size(); +} + +bool +TlsSession::setup() +{ + // Setup FSM + fsmHandlers_[TlsSessionState::SETUP] = [this](TlsSessionState s){ return handleStateSetup(s); }; + fsmHandlers_[TlsSessionState::COOKIE] = [this](TlsSessionState s){ return handleStateCookie(s); }; + fsmHandlers_[TlsSessionState::HANDSHAKE] = [this](TlsSessionState s){ return handleStateHandshake(s); }; + fsmHandlers_[TlsSessionState::ESTABLISHED] = [this](TlsSessionState s){ return handleStateEstablished(s); }; + fsmHandlers_[TlsSessionState::SHUTDOWN] = [this](TlsSessionState s){ return handleStateShutdown(s); }; + + return true; +} + +void +TlsSession::cleanup() +{ + state_ = TlsSessionState::SHUTDOWN; // be sure to block any user operations + + // Flush pending application send requests with a 0 bytes-sent result + for (auto& txdata : txQueue_) { + if (txdata.onComplete) + txdata.onComplete(0); + } + + if (session_) { + // DTLS: not use GNUTLS_SHUT_RDWR to not wait for a peer answer + gnutls_bye(session_, GNUTLS_SHUT_WR); + gnutls_deinit(session_); + session_ = nullptr; + } + + if (cookie_key_.data) + gnutls_free(cookie_key_.data); +} + +TlsSessionState +TlsSession::handleStateSetup(UNUSED TlsSessionState state) +{ + RING_DBG("[TLS] Start %s DTLS session", typeName()); + + try { + initCredentials(); + } catch (const std::exception& e) { + RING_ERR("[TLS] credential init failed: %s", e.what()); + return TlsSessionState::SHUTDOWN; + } + + if (isServer_) + return setupServer(); + else + return setupClient(); +} + +TlsSessionState +TlsSession::handleStateCookie(TlsSessionState state) +{ + RING_DBG("[TLS] SYN cookie"); + + std::size_t count; + { + // block until rx packet or shutdown + std::unique_lock<std::mutex> lk {ioMutex_}; + ioCv_.wait(lk, [this]{ return !rxQueue_.empty() or state_ == TlsSessionState::SHUTDOWN; }); + if (rxQueue_.empty()) + return TlsSessionState::SHUTDOWN; + count = rxQueue_.front().size(); + } + + // Total bytes rx during cookie checking (see flood protection below) + cookie_count_ += count; + + int ret; + + // Peek and verify front packet + { + std::lock_guard<std::mutex> lk {ioMutex_}; + auto& pkt = rxQueue_.front(); + std::memset(&prestate_, 0, sizeof(prestate_)); + ret = gnutls_dtls_cookie_verify(&cookie_key_, nullptr, 0, + pkt.data(), pkt.size(), &prestate_); + } + + if (ret < 0) { + gnutls_dtls_cookie_send(&cookie_key_, nullptr, 0, &prestate_, + this, + [](gnutls_transport_ptr_t t, const void* d, + size_t s) -> ssize_t { + auto this_ = reinterpret_cast<TlsSession*>(t); + return this_->sendRaw(d, s); + }); + + // Drop front packet + { + std::lock_guard<std::mutex> lk {ioMutex_}; + rxQueue_.pop_front(); + } + + // Cookie may be sent on multiple network packets + // So we retry until we get a valid cookie. + // To protect against a flood attack we delay each retry after FLOOD_THRESHOLD rx bytes. + if (cookie_count_ >= FLOOD_THRESHOLD) { + RING_WARN("[TLS] flood threshold reach (retry in %zds)", + std::chrono::duration_cast<std::chrono::seconds>(FLOOD_PAUSE).count()); + dump_io_stats(); + std::this_thread::sleep_for(FLOOD_PAUSE); // flood attack protection + } + return state; + } + + RING_DBG("[TLS] cookie ok"); + + ret = gnutls_init(&session_, GNUTLS_SERVER | GNUTLS_DATAGRAM); + if (ret != GNUTLS_E_SUCCESS) { + RING_ERR("[TLS] session init failed: %s", gnutls_strerror(ret)); + return TlsSessionState::SHUTDOWN; + } + + gnutls_certificate_server_set_request(session_, GNUTLS_CERT_REQUIRE); + gnutls_dtls_prestate_set(session_, &prestate_); + + if (not commonSessionInit()) + return TlsSessionState::SHUTDOWN; + + return TlsSessionState::HANDSHAKE; +} + +TlsSessionState +TlsSession::handleStateHandshake(TlsSessionState state) +{ + RING_DBG("[TLS] handshake"); + + auto ret = gnutls_handshake(session_); + + if (ret == GNUTLS_E_SUCCESS) { + auto desc = gnutls_session_get_desc(session_); + RING_WARN("[TLS] Session established: %s", desc); + gnutls_free(desc); + + // Aware about certificates updates + if (callbacks_.onCertificatesUpdate) { + unsigned int remote_count; + auto local = gnutls_certificate_get_ours(session_); + auto remote = gnutls_certificate_get_peers(session_, &remote_count); + callbacks_.onCertificatesUpdate(local, remote, remote_count); + } + + return TlsSessionState::ESTABLISHED; + } + + if (gnutls_error_is_fatal(ret)) { + RING_ERR("[TLS] handshake failed: %s", gnutls_strerror(ret)); + dump_io_stats(); + return TlsSessionState::SHUTDOWN; + } + + // TODO: handle GNUTLS_E_LARGE_PACKET (MTU must be lowered) + return state; +} + +TlsSessionState +TlsSession::handleStateEstablished(TlsSessionState state) +{ + // block until rx/tx packet or state change + std::unique_lock<std::mutex> lk {ioMutex_}; + ioCv_.wait(lk, [this]{ return !txQueue_.empty() or !rxQueue_.empty() or state_ != TlsSessionState::ESTABLISHED; }); + state = state_.load(); + if (state != TlsSessionState::ESTABLISHED) + return state; + + // Handle TX data from application + if (not txQueue_.empty()) { + decltype(txQueue_) tx_queue = std::move(txQueue_); + txQueue_.clear(); + lk.unlock(); + for (const auto& txdata : tx_queue) { + while (state_ == TlsSessionState::ESTABLISHED) { + auto bytes_sent = send(txdata); + auto fatal = gnutls_error_is_fatal(bytes_sent); + if (bytes_sent < 0 and !fatal) + continue; + if (txdata.onComplete) + txdata.onComplete(bytes_sent); + if (fatal) + return TlsSessionState::SHUTDOWN; + break; + } + } + lk.lock(); + } + + // Handle RX data from network + if (!rxQueue_.empty()) { + std::vector<uint8_t> buf(8*1024); + unsigned char sequence[8]; + + lk.unlock(); + auto ret = gnutls_record_recv_seq(session_, buf.data(), buf.size(), sequence); + if (ret > 0) { + buf.resize(ret); + // TODO: handle sequence re-order + if (callbacks_.onRxData) + callbacks_.onRxData(std::move(buf)); + return state; + } + + if (ret == 0) { + RING_DBG("[TLS] eof"); + return TlsSessionState::SHUTDOWN; + } + + if (ret == GNUTLS_E_REHANDSHAKE) { + RING_DBG("[TLS] re-handshake"); + return TlsSessionState::HANDSHAKE; + } + + if (gnutls_error_is_fatal(ret)) { + RING_ERR("[TLS] fatal error in recv: %s", gnutls_strerror(ret)); + return TlsSessionState::SHUTDOWN; + } + + // non-fatal error... let's continue + lk.lock(); + } + + return state; +} + +TlsSessionState +TlsSession::handleStateShutdown(TlsSessionState state) +{ + RING_DBG("[TLS] shutdown"); + + // Stop ourself + thread_.stop(); + return state; +} + +void +TlsSession::process() +{ + auto old_state = state_.load(); + auto new_state = fsmHandlers_[old_state](old_state); + + // update state_ with taking care for external state change + if (not std::atomic_compare_exchange_strong(&state_, &old_state, new_state)) + new_state = state_; + + if (old_state != new_state and callbacks_.onStateChange) + callbacks_.onStateChange(new_state); +} + + +TlsParams::DhParams +newDhParams() +{ + using clock = std::chrono::high_resolution_clock; + + auto bits = gnutls_sec_param_to_pk_bits(GNUTLS_PK_DH, /* GNUTLS_SEC_PARAM_HIGH */ GNUTLS_SEC_PARAM_NORMAL); + RING_DBG("Generating DH params with %u bits", bits); + auto start = clock::now(); + + gnutls_dh_params_t new_params_; + int ret = gnutls_dh_params_init(&new_params_); + if (ret != GNUTLS_E_SUCCESS) { + RING_ERR("Error initializing DH params: %s", gnutls_strerror(ret)); + return {nullptr, gnutls_dh_params_deinit}; + } + + ret = gnutls_dh_params_generate2(new_params_, bits); + if (ret != GNUTLS_E_SUCCESS) { + RING_ERR("Error generating DH params: %s", gnutls_strerror(ret)); + return {nullptr, gnutls_dh_params_deinit}; + } + + std::chrono::duration<double> time_span = clock::now() - start; + RING_DBG("Generated DH params with %u bits in %lfs", bits, time_span.count()); + return {new_params_, gnutls_dh_params_deinit}; +} + +}} // namespace ring::tls diff --git a/src/security/tls_session.h b/src/security/tls_session.h new file mode 100644 index 0000000000000000000000000000000000000000..5bd2b5b7171bef4e785434455f7f102b71755ea5 --- /dev/null +++ b/src/security/tls_session.h @@ -0,0 +1,198 @@ +/* + * Copyright (C) 2016 Savoir-faire Linux Inc. + * + * Author: Adrien Béraud <adrien.beraud@savoirfairelinux.com> + * Author: Guillaume Roguez <guillaume.roguez@savoirfairelinux.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 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 General Public License for more details. + * + * You should have received a copy of the GNU 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. + */ + +#pragma once + +#include "threadloop.h" + +#include <gnutls/gnutls.h> +#include <gnutls/dtls.h> +#include <gnutls/abstract.h> + +#include <string> +#include <list> +#include <functional> +#include <memory> +#include <mutex> +#include <condition_variable> +#include <chrono> +#include <future> +#include <utility> +#include <vector> +#include <map> + +namespace ring { +class IceTransport; +class IceSocket; +} // namespace ring + +namespace dht { namespace crypto { +class Certificate; +class PrivateKey; +}} // namespace dht::crypto + +namespace ring { namespace tls { + +enum class TlsSessionState { + SETUP, + COOKIE, // server only + HANDSHAKE, + ESTABLISHED, + SHUTDOWN +}; + +struct TlsParams { + using DhParams = std::unique_ptr<gnutls_dh_params_int, decltype(gnutls_dh_params_deinit)&>; + + // User CA list for session credentials + std::string ca_list; + + // User identity for credential + std::shared_ptr<dht::crypto::Certificate> cert; + std::shared_ptr<dht::crypto::PrivateKey> cert_key; + + // Diffie-Hellman computed by gnutls_dh_params_init/gnutls_dh_params_generateX + std::shared_future<DhParams> dh_params; + + // DTLS timeout + std::chrono::steady_clock::duration timeout; + + // Callback for certificate checkings + std::function<int(unsigned status, + const gnutls_datum_t* cert_list, + unsigned cert_list_size)> cert_check; +}; + +// Compute Diffie-Hellman parameters suitable for TlsParams::dh_params. +// Synchonous call: turn it asynchronous with std::async(std::launch::async, newDhParams) +TlsParams::DhParams newDhParams(); + +/** + * TlsSession + * + * Manages a DTLS connection over an ICE transport. + * This implementation uses a Threadloop to manage IO from ICE and TLS states, + * so IO are asynchronous. + */ +class TlsSession { +public: + using OnStateChangeFunc = std::function<void(TlsSessionState)>; + using OnRxDataFunc = std::function<void(std::vector<uint8_t>&&)>; + using OnCertificatesUpdate = std::function<void(const gnutls_datum_t*, const gnutls_datum_t*, unsigned int)>; + using VerifyCertificate = std::function<int(gnutls_session_t)>; + using TxDataCompleteFunc = std::function<void(std::size_t bytes_sent)>; + + // ===> WARNINGS <=== + // Following callbacks are called into the FSM thread context + // Do not call blocking routines inside them. + using TlsSessionCallbacks = struct { + OnStateChangeFunc onStateChange; + OnRxDataFunc onRxData; + OnCertificatesUpdate onCertificatesUpdate; + VerifyCertificate verifyCertificate; + }; + + TlsSession(std::shared_ptr<IceTransport> ice, int ice_comp_id, const TlsParams& params, + const TlsSessionCallbacks& cbs); + ~TlsSession(); + + // Returns the TLS session type ('server' or 'client') + const char* typeName() const; + + // Request TLS thread to stop and quit. IO are not possible after that. + void shutdown(); + + // Can be called by onStateChange callback when state == ESTABLISHED + // to obtain the used cypher suite id. + // Return the name of current cipher. + const char* getCurrentCipherSuiteId(std::array<uint8_t, 2>& cs_id) const; + + // Asynchronous sending operation. on_send_complete will be called with a positive number + // for number of bytes sent, or negative for errors, or 0 in case of shutdown (end of session). + ssize_t async_send(void* data, std::size_t size, TxDataCompleteFunc on_send_complete); + +private: + using clock = std::chrono::steady_clock; + using StateHandler = std::function<TlsSessionState(TlsSessionState state)>; + + // Constants (ctor init.) + const std::unique_ptr<IceSocket> socket_; + const bool isServer_; + const TlsParams params_; + const TlsSessionCallbacks callbacks_; + + // State machine + TlsSessionState handleStateSetup(TlsSessionState state); + TlsSessionState handleStateCookie(TlsSessionState state); + TlsSessionState handleStateHandshake(TlsSessionState state); + TlsSessionState handleStateEstablished(TlsSessionState state); + TlsSessionState handleStateShutdown(TlsSessionState state); + std::map<TlsSessionState, StateHandler> fsmHandlers_ {}; + std::atomic<TlsSessionState> state_ {TlsSessionState::SETUP}; + + // IO GnuTLS <-> ICE + struct TxData { + void* const ptr; + std::size_t size; + TxDataCompleteFunc onComplete; + }; + + std::mutex ioMutex_ {}; + std::condition_variable ioCv_ {}; + std::list<TxData> txQueue_ {}; + std::list<std::vector<uint8_t>> rxQueue_ {}; + + ssize_t send(const TxData&); + ssize_t sendRaw(const void*, size_t); + ssize_t sendRawVec(const giovec_t*, int); + ssize_t recvRaw(void*, size_t); + int waitForRawData(unsigned); + + // Statistics (also protected by mutex ioMutex_) + std::size_t stRxRawPacketCnt_ {0}; + std::size_t stRxRawBytesCnt_ {0}; + std::size_t stRxRawPacketDropCnt_ {0}; + std::size_t stTxRawPacketCnt_ {0}; + std::size_t stTxRawBytesCnt_ {0}; + void dump_io_stats() const; + + // GnuTLS backend and connection state + class TlsCertificateCredendials; + std::unique_ptr<TlsCertificateCredendials> xcred_; // ctor init. + gnutls_session_t session_ {nullptr}; + gnutls_datum_t cookie_key_ {nullptr, 0}; + gnutls_priority_t priority_cache_ {nullptr}; + gnutls_dtls_prestate_st prestate_ {}; + ssize_t cookie_count_ {0}; + + TlsSessionState setupClient(); + TlsSessionState setupServer(); + void initCredentials(); + bool commonSessionInit(); + + // FSM thread (TLS states) + ThreadLoop thread_; // ctor init. + bool setup(); + void process(); + void cleanup(); +}; + +}} // namespace ring::tls diff --git a/src/sip/sip_utils.h b/src/sip/sip_utils.h index 1d6510046e1e6ee1c4cb821f0e82c401cee7c1ab..5ff5a1d7bac05ab8e39733149cb9b3e2f02c85c7 100644 --- a/src/sip/sip_utils.h +++ b/src/sip/sip_utils.h @@ -27,6 +27,8 @@ #include <pjsip/sip_msg.h> #include <pjlib.h> +#include <pj/pool.h> +#include <pjsip/sip_endpoint.h> #include <utility> #include <string> @@ -95,6 +97,16 @@ private: pjsip_dialog* dialog_; }; +// Helper on PJSIP memory pool allocation from endpoint +// This encapsulate the allocated memory pool inside a unique_ptr +static inline std::unique_ptr<pj_pool_t, decltype(pj_pool_release)&> +smart_alloc_pool(pjsip_endpoint* endpt, const char* const name, pj_size_t initial, pj_size_t inc) { + auto pool = pjsip_endpt_create_pool(endpt, name, initial, inc); + if (not pool) + throw std::bad_alloc(); + return std::unique_ptr<pj_pool_t, decltype(pj_pool_release)&>(pool, pj_pool_release); +} + }} // namespace ring::sip_utils #endif // SIP_UTILS_H_