diff --git a/src/jamidht/account_manager.cpp b/src/jamidht/account_manager.cpp index 14b65e4de72a185fbcd0f6aed32f48dcbe3388ce..fdcac969fcb03725175e59f24683a2e746231765 100644 --- a/src/jamidht/account_manager.cpp +++ b/src/jamidht/account_manager.cpp @@ -31,6 +31,7 @@ #include <exception> #include <future> #include <fstream> +#include <gnutls/ocsp.h> namespace jami { @@ -300,6 +301,12 @@ AccountManager::foundPeerDevice(const std::shared_ptr<dht::crypto::Certificate>& return false; } + // Check cached OCSP response + if (crt->ocspResponse and crt->ocspResponse->getCertificateStatus() != GNUTLS_OCSP_CERT_GOOD){ + JAMI_ERR("Certificate %s is disabled by cached OCSP response", crt->getId().to_c_str()); + return false; + } + account_id = crt->issuer->getId(); JAMI_WARN("Found peer device: %s account:%s CA:%s", crt->getId().toString().c_str(), diff --git a/src/jamidht/contact_list.cpp b/src/jamidht/contact_list.cpp index 1ceb92c992ef41be04dd8218dde4aa556add4f5a..bacf4937bcf8412e990d7b35f8d19f122801594c 100644 --- a/src/jamidht/contact_list.cpp +++ b/src/jamidht/contact_list.cpp @@ -23,6 +23,7 @@ #include "account_const.h" #include <fstream> +#include <gnutls/ocsp.h> namespace jami { @@ -418,6 +419,21 @@ ContactList::foundAccountDevice(const std::shared_ptr<dht::crypto::Certificate>& name.c_str(), crt->getId().toString().c_str()); tls::CertificateStore::instance().pinCertificate(crt); + if (crt->ocspResponse){ + unsigned int status = crt->ocspResponse->getCertificateStatus(); + if (status == GNUTLS_OCSP_CERT_GOOD){ + JAMI_DBG("Certificate %s has good OCSP status", crt->getId().to_c_str()); + trust_.setCertificateStatus(crt->getId().toString(), tls::TrustStore::PermissionStatus::ALLOWED); + } + else if (status == GNUTLS_OCSP_CERT_REVOKED){ + JAMI_ERR("Certificate %s has revoked OCSP status", crt->getId().to_c_str()); + trust_.setCertificateStatus(crt->getId().toString(), tls::TrustStore::PermissionStatus::BANNED); + } + else { + JAMI_ERR("Certificate %s has unknown OCSP status", crt->getId().to_c_str()); + trust_.setCertificateStatus(crt->getId().toString(), tls::TrustStore::PermissionStatus::UNDEFINED); + } + } saveKnownDevices(); callbacks_.devicesChanged(knownDevices_); } else { diff --git a/src/security/certstore.cpp b/src/security/certstore.cpp index b4ac17b935ce61dc89664556c83aedadd186de07..e3f11bb97e7dbb6173205219207757f86f519e67 100644 --- a/src/security/certstore.cpp +++ b/src/security/certstore.cpp @@ -2,6 +2,7 @@ * Copyright (C) 2004-2020 Savoir-faire Linux Inc. * * Author: Adrien Béraud <adrien.beraud@savoirfairelinux.com> + * Author: Vsevolod Ivanov <vsevolod.ivanov@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 @@ -26,6 +27,7 @@ #include "logger.h" #include <opendht/thread_pool.h> +#include <gnutls/ocsp.h> #include <thread> #include <sstream> @@ -47,9 +49,11 @@ CertificateStore::instance() CertificateStore::CertificateStore() : certPath_(fileutils::get_data_dir() + DIR_SEPARATOR_CH + "certificates") , crlPath_(fileutils::get_data_dir() + DIR_SEPARATOR_CH + "crls") + , ocspPath_(fileutils::get_data_dir() + DIR_SEPARATOR_CH + "ocsp") { fileutils::check_dir(certPath_.c_str()); fileutils::check_dir(crlPath_.c_str()); + fileutils::check_dir(ocspPath_.c_str()); loadLocalCertificates(); } @@ -94,6 +98,32 @@ CertificateStore::loadRevocations(crypto::Certificate& crt) const JAMI_WARN("Can't load revocation list: %s", e.what()); } } + auto ocsp_dir = ocspPath_+DIR_SEPARATOR_CH+crt.getId().toString(); + auto ocsp_dir_content = fileutils::readDirectory(ocsp_dir); + for (const auto& ocsp /*filename*/ : ocsp_dir_content) { + try { + std::string ocsp_filepath = ocsp_dir+DIR_SEPARATOR_CH+ocsp; + JAMI_DBG("Found %s", ocsp_filepath.c_str()); + auto serial = crt.getSerialNumber(); + if (dht::toHex(serial.data(), serial.size()) != ocsp) + continue; + // Save the response + dht::Blob ocspBlob = fileutils::loadFile(ocsp_filepath); + crt.ocspResponse = std::make_shared<dht::crypto::OcspResponse>(ocspBlob.data(),ocspBlob.size()); + unsigned int status = crt.ocspResponse->getCertificateStatus(); + if (status == GNUTLS_OCSP_CERT_GOOD) + JAMI_DBG("Certificate %s has good OCSP status", crt.getId().to_c_str()); + else if (status == GNUTLS_OCSP_CERT_REVOKED) + JAMI_ERR("Certificate %s has revoked OCSP status", crt.getId().to_c_str()); + else if (status == GNUTLS_OCSP_CERT_UNKNOWN) + JAMI_ERR("Certificate %s has unknown OCSP status", crt.getId().to_c_str()); + else + JAMI_ERR("Certificate %s has invalid OCSP status", crt.getId().to_c_str()); + + } catch (const std::exception& e) { + JAMI_WARN("Can't load OCSP revocation status: %s", e.what()); + } + } } std::vector<std::string> @@ -367,6 +397,35 @@ CertificateStore::pinRevocationList(const std::string& id, const dht::crypto::Re crl.getPacked()); } +void +CertificateStore::pinOcspResponse(const dht::crypto::Certificate& cert) +{ + if (not cert.ocspResponse) + return; + try { + cert.ocspResponse->getCertificateStatus(); + } + catch (dht::crypto::CryptoException& e){ + JAMI_ERR("Failed to read certificate status of OCSP response: %s", e.what()); + return; + } + auto id = cert.getId().toString(); + auto serialhex = dht::toHex(cert.getSerialNumber()); + auto dir = ocspPath_ + DIR_SEPARATOR_CH + id; + dht::ThreadPool::io().run([ + path = dir + DIR_SEPARATOR_CH + serialhex, + dir = std::move(dir), + id = std::move(id), + serialhex = std::move(serialhex), + ocspResponse = cert.ocspResponse + ]{ + JAMI_DBG("Saving OCSP Response of device %s with serial %s", id.c_str(), serialhex.c_str()); + std::lock_guard<std::mutex> lock(fileutils::getFileLock(path)); + fileutils::check_dir(dir.c_str()); + fileutils::saveFile(path, ocspResponse->pack()); + }); +} + TrustStore::PermissionStatus TrustStore::statusFromStr(const char* str) { diff --git a/src/security/certstore.h b/src/security/certstore.h index 3e34fc6e90cd11960b90b5c6f478a823e34443cc..24642dc9d3d3e6b9e1342a93447af8a7629cc166 100644 --- a/src/security/certstore.h +++ b/src/security/certstore.h @@ -83,6 +83,7 @@ public: std::make_shared<dht::crypto::RevocationList>( std::forward<dht::crypto::RevocationList>(crl))); } + void pinOcspResponse(const dht::crypto::Certificate& cert); void loadRevocations(crypto::Certificate& crt) const; @@ -94,6 +95,7 @@ private: const std::string certPath_; const std::string crlPath_; + const std::string ocspPath_; mutable std::mutex lock_; std::map<std::string, std::shared_ptr<crypto::Certificate>> certs_; diff --git a/src/security/tls_session.cpp b/src/security/tls_session.cpp index 7feb9362e40cb13815ce666a2b5ead7dc6e9df37..9c737eb555ed95dac99d9a80a16d92cd076d68a4 100644 --- a/src/security/tls_session.cpp +++ b/src/security/tls_session.cpp @@ -4,6 +4,7 @@ * Author: Adrien Béraud <adrien.beraud@savoirfairelinux.com> * Author: Guillaume Roguez <guillaume.roguez@savoirfairelinux.com> * Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com> + * Author: Vsevolod Ivanov <vsevolod.ivanov@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 @@ -38,6 +39,10 @@ #include <gnutls/dtls.h> #include <gnutls/abstract.h> +#include <gnutls/crypto.h> +#include <gnutls/ocsp.h> +#include <opendht/http.h> + #include <list> #include <mutex> #include <condition_variable> @@ -103,6 +108,8 @@ static constexpr auto RX_OOO_TIMEOUT = std::chrono::milliseconds(1500); static constexpr int ASYMETRIC_TRANSPORT_MTU_OFFSET = 20; // when client, if your local IP is IPV4 and server is IPV6; you must reduce your MTU to // avoid packet too big error on server side. the offset is the difference in size of IP headers +static constexpr auto OCSP_VERIFICATION_TIMEOUT = std::chrono::seconds(2); // Time to wait for an OCSP verification +static constexpr auto OCSP_REQUEST_TIMEOUT = std::chrono::seconds(2); // Time to wait for an ocsp-request // Helper to cast any duration into an integer number of milliseconds template<class Rep, class Period> @@ -203,6 +210,8 @@ class TlsSession::TlsSessionImpl public: using clock = std::chrono::steady_clock; using StateHandler = std::function<TlsSessionState(TlsSessionState state)>; + using OcspVerification = std::function<void(const int status)>; + using HttpResponse = std::function<void(const dht::http::Response& response)>; // Constants (ctor init.) const bool isServer_; @@ -284,6 +293,20 @@ public: void initCredentials(); bool commonSessionInit(); + /* + * Implicit certificate validations. + */ + int verifyCertificateWrapper(gnutls_session_t session); + /* + * Verify OCSP (Online Certificate Service Protocol): + */ + void verifyOcsp(const std::string& url, dht::crypto::Certificate& cert, gnutls_x509_crt_t issuer, OcspVerification cb); + /* + * Send OCSP Request to the specified URI. + */ + void sendOcspRequest(const std::string& uri, std::string body, + std::chrono::seconds timeout, HttpResponse cb = {}); + // FSM thread (TLS states) ThreadLoop thread_; // ctor init. bool setup(); @@ -298,6 +321,9 @@ public: int hbPingRecved_ {0}; bool pmtudOver_ {false}; void pathMtuHeartbeat(); + + std::mutex requestsMtx_; + std::set<std::shared_ptr<dht::http::Request>> requests_; }; TlsSession::TlsSessionImpl::TlsSessionImpl(std::unique_ptr<SocketType>&& transport, @@ -441,11 +467,10 @@ TlsSession::TlsSessionImpl::initCredentials() // 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<TlsSessionImpl*>(gnutls_session_get_ptr(session)); - return this_->callbacks_.verifyCertificate(session); - }); + gnutls_certificate_set_verify_function(*xcred_, [](gnutls_session_t session) -> int { + auto this_ = reinterpret_cast<TlsSessionImpl*>(gnutls_session_get_ptr(session)); + return this_->verifyCertificateWrapper(session); + }); // Load user-given CA list if (not params_.ca_list.empty()) { @@ -589,6 +614,200 @@ TlsSession::TlsSessionImpl::commonSessionInit() return true; } +std::string +getOcspUrl(gnutls_x509_crt_t cert) +{ + int ret; + gnutls_datum_t aia; + unsigned int seq = 0; + do { + // Extracts the Authority Information Access (AIA) extension, see RFC 5280 section 4.2.2.1 + ret = gnutls_x509_crt_get_authority_info_access(cert, seq++, GNUTLS_IA_OCSP_URI, &aia, NULL); + } + while (ret < 0 && ret != GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE); + // could also try the issuer if we include ocsp uri into there + if (ret < 0) { + return {}; + } + std::string url((const char*)aia.data,(size_t)aia.size); + gnutls_free(aia.data); + return url; +} + +int +TlsSession::TlsSessionImpl::verifyCertificateWrapper(gnutls_session_t session) +{ + // Perform user-set verification first to avoid flooding with ocsp-requests if peer is denied + int verified; + if (callbacks_.verifyCertificate) + { + auto this_ = reinterpret_cast<TlsSessionImpl*>(gnutls_session_get_ptr(session)); + verified = this_->callbacks_.verifyCertificate(session); + if (verified != GNUTLS_E_SUCCESS) + return verified; + } + /* + * Support only x509 format + */ + if (gnutls_certificate_type_get(session) != GNUTLS_CRT_X509) + return GNUTLS_E_CERTIFICATE_ERROR; + /* + * Get the peer's raw certificate (chain) as sent by the peer. + * The first certificate in the list is the peer's certificate, following the issuer's cert. etc. + */ + unsigned int cert_list_size = 0; + auto cert_list = gnutls_certificate_get_peers(session, &cert_list_size); + if (cert_list == nullptr) + return GNUTLS_E_CERTIFICATE_ERROR; + + /* + * Extract verification data by deserializing the certificate chain + */ + std::vector<std::pair<uint8_t*, uint8_t*>> crt_data; + crt_data.reserve(cert_list_size); + for (unsigned i = 0; i < cert_list_size; i++) + crt_data.emplace_back(cert_list[i].data, cert_list[i].data + cert_list[i].size); + auto cert = dht::crypto::Certificate(crt_data); + + std::string ocspUrl = getOcspUrl(cert.cert); + if (ocspUrl.empty()) { + JAMI_DBG("Skipping OCSP verification %s: AIA not found", cert.getUID().c_str()); + return verified; + } + + // OCSP (Online Certificate Service Protocol) { + bool ocsp_done = false; + std::mutex cv_m; + std::condition_variable cv; + std::unique_lock<std::mutex> cv_lk(cv_m); + + gnutls_x509_crt_t issuer_crt = cert.issuer ? cert.issuer->cert : nullptr; + verifyOcsp(ocspUrl, cert, issuer_crt, [&](const int status) { + if (status == GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE){ + // OCSP URI is absent, don't fail the verification by overwritting the user-set one. + JAMI_WARN("Skipping OCSP verification %s: request failed", cert.getUID().c_str()); + } else { + if (status != GNUTLS_E_SUCCESS) { + JAMI_ERR("OCSP verification failed for %s: %s (%i)", + cert.getUID().c_str(), gnutls_strerror(status), status); + } + verified = status; + } + std::lock_guard<std::mutex> cv_lk(cv_m); + ocsp_done = true; + cv.notify_all(); + }); + cv.wait_for(cv_lk, std::chrono::seconds(OCSP_VERIFICATION_TIMEOUT),[&ocsp_done]{return ocsp_done;}); + // } OCSP + + return verified; +} + +void +TlsSession::TlsSessionImpl::verifyOcsp(const std::string& aia_uri, dht::crypto::Certificate& cert, gnutls_x509_crt_t issuer, OcspVerification cb) +{ + JAMI_DBG("Certificate's AIA URI: %s", aia_uri.c_str()); + + // Generate OCSP request + std::pair<std::string, dht::Blob> ocsp_req; + try { + ocsp_req = cert.generateOcspRequest(issuer); + } + catch (dht::crypto::CryptoException& e){ + JAMI_ERR("Failed to generate OCSP request: %s", e.what()); + if (cb) + cb(GNUTLS_E_INVALID_REQUEST); + return; + } + + sendOcspRequest(aia_uri, std::move(ocsp_req.first), OCSP_REQUEST_TIMEOUT, + [this, cb = std::move(cb), &cert, nonce=std::move(ocsp_req.second)](const dht::http::Response& r){ + // Prepare response data + // Verify response validity + if (r.status_code != 200) { + JAMI_WARN("HTTP OCSP Request Failed with code %i", r.status_code); + if (cb) + cb(GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE); + return; + } + JAMI_DBG("HTTP OCSP Request done!"); + unsigned int verify = 0; + try { + cert.ocspResponse = std::make_shared<dht::crypto::OcspResponse>((const uint8_t*)r.body.data(), r.body.size()); + JAMI_DBG("%s", cert.ocspResponse->toString().c_str()); + verify = cert.ocspResponse->verifyDirect(cert, nonce); + } + catch (dht::crypto::CryptoException& e) { + JAMI_ERR("Failed to verify OCSP response: %s", e.what()); + if (cb) + cb(GNUTLS_E_INVALID_REQUEST); + return; + } + if (verify == 0) + JAMI_DBG("OCSP verification success!"); + else + JAMI_ERR("OCSP verification error!"); + if (verify & GNUTLS_OCSP_VERIFY_SIGNER_NOT_FOUND) + JAMI_ERR("Signer cert not found"); + if (verify & GNUTLS_OCSP_VERIFY_SIGNER_KEYUSAGE_ERROR) + JAMI_ERR("Signer cert keyusage error"); + if (verify & GNUTLS_OCSP_VERIFY_UNTRUSTED_SIGNER) + JAMI_ERR("Signer cert is not trusted"); + if (verify & GNUTLS_OCSP_VERIFY_INSECURE_ALGORITHM) + JAMI_ERR("Insecure algorithm"); + if (verify & GNUTLS_OCSP_VERIFY_SIGNATURE_FAILURE) + JAMI_ERR("Signature failure"); + if (verify & GNUTLS_OCSP_VERIFY_CERT_NOT_ACTIVATED) + JAMI_ERR("Signer cert not yet activated"); + if (verify & GNUTLS_OCSP_VERIFY_CERT_EXPIRED) + JAMI_ERR("Signer cert expired"); + // Save response into the certificate store + tls::CertificateStore::instance().pinOcspResponse(cert); + if (cb) + cb(verify); + }); +} + +void +TlsSession::TlsSessionImpl::sendOcspRequest(const std::string& uri, std::string body, + std::chrono::seconds timeout, HttpResponse cb) +{ + using namespace dht; + auto request = std::make_shared<http::Request>(*Manager::instance().ioContext(), uri);//, logger); + request->set_method(restinio::http_method_post()); + request->set_header_field(restinio::http_field_t::user_agent, "Jami"); + request->set_header_field(restinio::http_field_t::accept, "*/*"); + request->set_header_field(restinio::http_field_t::content_type, "application/ocsp-request"); + request->set_body(std::move(body)); + request->set_connection_type(restinio::http_connection_header_t::close); + request->add_on_state_change_callback([this, cb = std::move(cb), timeout] + (const http::Request::State state, const http::Response response) + { + JAMI_DBG("HTTP OCSP Request state=%i status_code=%i", (unsigned int) state, response.status_code); + if (state == http::Request::State::SENDING){ + auto request = response.request.lock(); + request->get_connection()->timeout(timeout, [request](const asio::error_code& ec){ + if (ec and ec != asio::error::operation_aborted) + JAMI_ERR("HTTP OCSP Request timeout with error: %s", ec.message().c_str()); + request->cancel(); + }); + } + if (state != http::Request::State::DONE) + return; + if (cb) + cb(response); + if (auto request = response.request.lock()) { + std::lock_guard<std::mutex> lock(requestsMtx_); + requests_.erase(request); + } + }); + { + std::lock_guard<std::mutex> lock(requestsMtx_); + requests_.emplace(request); + } + request->send(); +} + std::size_t TlsSession::TlsSessionImpl::send(const ValueType* tx_data, std::size_t tx_size, std::error_code& ec) { diff --git a/src/security/tls_session.h b/src/security/tls_session.h index 823e32aed3a869b94a3d99aeff924e6096ad2034..5951a4008f9382af3f9efa9b96ebb30b1e2dd568 100644 --- a/src/security/tls_session.h +++ b/src/security/tls_session.h @@ -4,6 +4,7 @@ * Author: Adrien Béraud <adrien.beraud@savoirfairelinux.com> * Author: Guillaume Roguez <guillaume.roguez@savoirfairelinux.com> * Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com> + * Author: Vsevolod Ivanov <vsevolod.ivanov@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