diff options
Diffstat (limited to 'contrib/epee/src/net_ssl.cpp')
-rw-r--r-- | contrib/epee/src/net_ssl.cpp | 270 |
1 files changed, 178 insertions, 92 deletions
diff --git a/contrib/epee/src/net_ssl.cpp b/contrib/epee/src/net_ssl.cpp index eb0b0ad65..7bedb18ac 100644 --- a/contrib/epee/src/net_ssl.cpp +++ b/contrib/epee/src/net_ssl.cpp @@ -46,8 +46,7 @@ namespace { void operator()(BIO* ptr) const noexcept { - if (ptr) - BIO_free(ptr); + BIO_free(ptr); } }; using openssl_bio = std::unique_ptr<BIO, openssl_bio_free>; @@ -56,12 +55,43 @@ namespace { void operator()(EVP_PKEY* ptr) const noexcept { - if (ptr) - EVP_PKEY_free(ptr); + EVP_PKEY_free(ptr); } }; using openssl_pkey = std::unique_ptr<EVP_PKEY, openssl_pkey_free>; + struct openssl_rsa_free + { + void operator()(RSA* ptr) const noexcept + { + RSA_free(ptr); + } + }; + using openssl_rsa = std::unique_ptr<RSA, openssl_rsa_free>; + + struct openssl_bignum_free + { + void operator()(BIGNUM* ptr) const noexcept + { + BN_free(ptr); + } + }; + using openssl_bignum = std::unique_ptr<BIGNUM, openssl_bignum_free>; + + boost::system::error_code load_ca_file(boost::asio::ssl::context& ctx, const std::string& path) + { + SSL_CTX* const ssl_ctx = ctx.native_handle(); // could be moved from context + if (ssl_ctx == nullptr) + return {boost::asio::error::invalid_argument}; + + if (!SSL_CTX_load_verify_locations(ssl_ctx, path.c_str(), nullptr)) + { + return boost::system::error_code{ + int(::ERR_get_error()), boost::asio::error::get_ssl_category() + }; + } + return boost::system::error_code{}; + } } namespace epee @@ -69,6 +99,7 @@ namespace epee namespace net_utils { + // https://stackoverflow.com/questions/256405/programmatically-create-x509-certificate-using-openssl bool create_ssl_certificate(EVP_PKEY *&pkey, X509 *&cert) { @@ -81,19 +112,37 @@ bool create_ssl_certificate(EVP_PKEY *&pkey, X509 *&cert) } openssl_pkey pkey_deleter{pkey}; - RSA *rsa = RSA_generate_key(4096, RSA_F4, NULL, NULL); + openssl_rsa rsa{RSA_new()}; if (!rsa) { + MERROR("Error allocating RSA private key"); + return false; + } + + openssl_bignum exponent{BN_new()}; + if (!exponent) + { + MERROR("Error allocating exponent"); + return false; + } + + BN_set_word(exponent.get(), RSA_F4); + + if (RSA_generate_key_ex(rsa.get(), 4096, exponent.get(), nullptr) != 1) + { MERROR("Error generating RSA private key"); return false; } - if (EVP_PKEY_assign_RSA(pkey, rsa) <= 0) // The RSA will be automatically freed when the EVP_PKEY structure is freed. + + if (EVP_PKEY_assign_RSA(pkey, rsa.get()) <= 0) { MERROR("Error assigning RSA private key"); - RSA_free(rsa); return false; } + // the RSA key is now managed by the EVP_PKEY structure + (void)rsa.release(); + cert = X509_new(); if (!cert) { @@ -122,22 +171,34 @@ bool create_ssl_certificate(EVP_PKEY *&pkey, X509 *&cert) return true; } -ssl_context_t create_ssl_context(const std::pair<std::string, std::string> &private_key_and_certificate_path, std::list<std::string> allowed_certificates, std::vector<std::vector<uint8_t>> allowed_fingerprints, bool allow_any_cert) +ssl_options_t::ssl_options_t(std::vector<std::vector<std::uint8_t>> fingerprints, std::string ca_path) + : fingerprints_(std::move(fingerprints)), + ca_path(std::move(ca_path)), + auth(), + support(ssl_support_t::e_ssl_support_enabled), + verification(ssl_verification_t::user_certificates) { - ssl_context_t ssl_context{boost::asio::ssl::context(boost::asio::ssl::context::tlsv12), std::move(allowed_certificates), std::move(allowed_fingerprints)}; + std::sort(fingerprints_.begin(), fingerprints_.end()); +} + +boost::asio::ssl::context ssl_options_t::create_context() const +{ + boost::asio::ssl::context ssl_context{boost::asio::ssl::context::tlsv12}; + if (!bool(*this)) + return ssl_context; // only allow tls v1.2 and up - ssl_context.context.set_options(boost::asio::ssl::context::default_workarounds); - ssl_context.context.set_options(boost::asio::ssl::context::no_sslv2); - ssl_context.context.set_options(boost::asio::ssl::context::no_sslv3); - ssl_context.context.set_options(boost::asio::ssl::context::no_tlsv1); - ssl_context.context.set_options(boost::asio::ssl::context::no_tlsv1_1); + ssl_context.set_options(boost::asio::ssl::context::default_workarounds); + ssl_context.set_options(boost::asio::ssl::context::no_sslv2); + ssl_context.set_options(boost::asio::ssl::context::no_sslv3); + ssl_context.set_options(boost::asio::ssl::context::no_tlsv1); + ssl_context.set_options(boost::asio::ssl::context::no_tlsv1_1); // only allow a select handful of tls v1.3 and v1.2 ciphers to be used - SSL_CTX_set_cipher_list(ssl_context.context.native_handle(), "ECDHE-ECDSA-CHACHA20-POLY1305-SHA256:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-CHACHA20-POLY1305"); + SSL_CTX_set_cipher_list(ssl_context.native_handle(), "ECDHE-ECDSA-CHACHA20-POLY1305-SHA256:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-CHACHA20-POLY1305"); // set options on the SSL context for added security - SSL_CTX *ctx = ssl_context.context.native_handle(); + SSL_CTX *ctx = ssl_context.native_handle(); CHECK_AND_ASSERT_THROW_MES(ctx, "Failed to get SSL context"); SSL_CTX_clear_options(ctx, SSL_OP_LEGACY_SERVER_CONNECT); // SSL_CTX_SET_OPTIONS(3) SSL_CTX_set_session_cache_mode(ctx, SSL_SESS_CACHE_OFF); // https://stackoverflow.com/questions/22378442 @@ -153,10 +214,29 @@ ssl_context_t create_ssl_context(const std::pair<std::string, std::string> &priv #ifdef SSL_OP_NO_COMPRESSION SSL_CTX_set_options(ctx, SSL_OP_NO_COMPRESSION); #endif - ssl_context.context.set_default_verify_paths(); - CHECK_AND_ASSERT_THROW_MES(private_key_and_certificate_path.first.empty() == private_key_and_certificate_path.second.empty(), "private key and certificate must be either both given or both empty"); - if (private_key_and_certificate_path.second.empty()) + switch (verification) + { + case ssl_verification_t::system_ca: + ssl_context.set_default_verify_paths(); + break; + case ssl_verification_t::user_certificates: + ssl_context.set_verify_depth(0); + /* fallthrough */ + case ssl_verification_t::user_ca: + if (!ca_path.empty()) + { + const boost::system::error_code err = load_ca_file(ssl_context, ca_path); + if (err) + throw boost::system::system_error{err, "Failed to load user CA file at " + ca_path}; + } + break; + default: + break; + } + + CHECK_AND_ASSERT_THROW_MES(auth.private_key_path.empty() == auth.certificate_path.empty(), "private key and certificate must be either both given or both empty"); + if (auth.private_key_path.empty()) { EVP_PKEY *pkey; X509 *cert; @@ -167,19 +247,15 @@ ssl_context_t create_ssl_context(const std::pair<std::string, std::string> &priv EVP_PKEY_free(pkey); } else - { - ssl_context.context.use_private_key_file(private_key_and_certificate_path.first, boost::asio::ssl::context::pem); - ssl_context.context.use_certificate_file(private_key_and_certificate_path.second, boost::asio::ssl::context::pem); - } - ssl_context.allow_any_cert = allow_any_cert; + auth.use_ssl_certificate(ssl_context); return ssl_context; } -void use_ssl_certificate(ssl_context_t &ssl_context, const std::pair<std::string, std::string> &private_key_and_certificate_path) +void ssl_authentication_t::use_ssl_certificate(boost::asio::ssl::context &ssl_context) const { - ssl_context.context.use_private_key_file(private_key_and_certificate_path.first, boost::asio::ssl::context::pem); - ssl_context.context.use_certificate_file(private_key_and_certificate_path.second, boost::asio::ssl::context::pem); + ssl_context.use_private_key_file(private_key_path, boost::asio::ssl::context::pem); + ssl_context.use_certificate_chain_file(certificate_path); } bool is_ssl(const unsigned char *data, size_t len) @@ -202,23 +278,44 @@ bool is_ssl(const unsigned char *data, size_t len) return false; } -bool is_certificate_allowed(boost::asio::ssl::verify_context &ctx, const ssl_context_t &ssl_context) +bool ssl_options_t::has_strong_verification(boost::string_ref host) const noexcept { - X509_STORE_CTX *sctx = ctx.native_handle(); - if (!sctx) - { - MERROR("Error getting verify_context handle"); - return false; - } - X509 *cert =X509_STORE_CTX_get_current_cert(sctx); - if (!cert) + // onion and i2p addresses contain information about the server cert + // which both authenticates and encrypts + if (host.ends_with(".onion") || host.ends_with(".i2p")) + return true; + switch (verification) { - MERROR("No certificate found in verify_context"); - return false; + default: + case ssl_verification_t::none: + case ssl_verification_t::system_ca: + return false; + case ssl_verification_t::user_certificates: + case ssl_verification_t::user_ca: + break; } + return true; +} +bool ssl_options_t::has_fingerprint(boost::asio::ssl::verify_context &ctx) const +{ // can we check the certificate against a list of fingerprints? - if (!ssl_context.allowed_fingerprints.empty()) { + if (!fingerprints_.empty()) { + X509_STORE_CTX *sctx = ctx.native_handle(); + if (!sctx) + { + MERROR("Error getting verify_context handle"); + return false; + } + + X509* cert = nullptr; + const STACK_OF(X509)* chain = X509_STORE_CTX_get_chain(sctx); + if (!chain || sk_X509_num(chain) < 1 || !(cert = sk_X509_value(chain, 0))) + { + MERROR("No certificate found in verify_context"); + return false; + } + // buffer for the certificate digest and the size of the result std::vector<uint8_t> digest(EVP_MAX_MD_SIZE); unsigned int size{ 0 }; @@ -232,74 +329,63 @@ bool is_certificate_allowed(boost::asio::ssl::verify_context &ctx, const ssl_con // strip unnecessary bytes from the digest digest.resize(size); - // is the certificate fingerprint inside the list of allowed fingerprints? - if (std::find(ssl_context.allowed_fingerprints.begin(), ssl_context.allowed_fingerprints.end(), digest) != ssl_context.allowed_fingerprints.end()) - return true; - } - - if (!ssl_context.allowed_certificates.empty()) { - BIO *bio_cert = BIO_new(BIO_s_mem()); - bool success = PEM_write_bio_X509(bio_cert, cert); - if (!success) - { - BIO_free(bio_cert); - MERROR("Failed to print certificate"); - return false; - } - BUF_MEM *buf = NULL; - BIO_get_mem_ptr(bio_cert, &buf); - if (!buf || !buf->data || !buf->length) - { - BIO_free(bio_cert); - MERROR("Failed to write certificate: " << ERR_get_error()); - return false; - } - std::string certificate(std::string(buf->data, buf->length)); - BIO_free(bio_cert); - if (std::find(ssl_context.allowed_certificates.begin(), ssl_context.allowed_certificates.end(), certificate) != ssl_context.allowed_certificates.end()) - return true; + return std::binary_search(fingerprints_.begin(), fingerprints_.end(), digest); } - // if either checklist is non-empty we must have failed it - return ssl_context.allowed_fingerprints.empty() && ssl_context.allowed_certificates.empty(); + return false; } -bool ssl_handshake(boost::asio::ssl::stream<boost::asio::ip::tcp::socket> &socket, boost::asio::ssl::stream_base::handshake_type type, const epee::net_utils::ssl_context_t &ssl_context) +bool ssl_options_t::handshake(boost::asio::ssl::stream<boost::asio::ip::tcp::socket> &socket, boost::asio::ssl::stream_base::handshake_type type, const std::string& host) const { - bool verified = false; socket.next_layer().set_option(boost::asio::ip::tcp::no_delay(true)); - socket.set_verify_mode(boost::asio::ssl::verify_peer); - socket.set_verify_callback([&](bool preverified, boost::asio::ssl::verify_context &ctx) + + /* Using system-wide CA store for client verification is funky - there is + no expected hostname for server to verify against. If server doesn't have + specific whitelisted certificates for client, don't require client to + send certificate at all. */ + const bool no_verification = verification == ssl_verification_t::none || + (type == boost::asio::ssl::stream_base::server && fingerprints_.empty() && ca_path.empty()); + + /* According to OpenSSL documentation (and SSL specifications), server must + always send certificate unless "anonymous" cipher mode is used which are + disabled by default. Either way, the certificate is never inspected. */ + if (no_verification) + socket.set_verify_mode(boost::asio::ssl::verify_none); + else { - if (!preverified) + socket.set_verify_mode(boost::asio::ssl::verify_peer | boost::asio::ssl::verify_fail_if_no_peer_cert); + + // in case server is doing "virtual" domains, set hostname + SSL* const ssl_ctx = socket.native_handle(); + if (type == boost::asio::ssl::stream_base::client && !host.empty() && ssl_ctx) + SSL_set_tlsext_host_name(ssl_ctx, host.c_str()); + + socket.set_verify_callback([&](const bool preverified, boost::asio::ssl::verify_context &ctx) { - const int err = X509_STORE_CTX_get_error(ctx.native_handle()); - const int depth = X509_STORE_CTX_get_error_depth(ctx.native_handle()); - if (err != X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT || depth != 0) + // preverified means it passed system or user CA check. System CA is never loaded + // when fingerprints are whitelisted. + const bool verified = preverified && + (verification != ssl_verification_t::system_ca || host.empty() || boost::asio::ssl::rfc2818_verification(host)(preverified, ctx)); + + if (!verified && !has_fingerprint(ctx)) { - MERROR("Invalid SSL certificate, error " << err << " at depth " << depth << ", connection dropped"); - return false; + // autodetect will reconnect without SSL - warn and keep connection encrypted + if (support != ssl_support_t::e_ssl_support_autodetect) + { + MERROR("SSL certificate is not in the allowed list, connection droppped"); + return false; + } + MWARNING("SSL peer has not been verified"); } - } - if (!ssl_context.allow_any_cert && !is_certificate_allowed(ctx, ssl_context)) - { - MERROR("Certificate is not in the allowed list, connection droppped"); - return false; - } - verified = true; - return true; - }); + return true; + }); + } boost::system::error_code ec; socket.handshake(type, ec); if (ec) { - MERROR("handshake failed, connection dropped: " << ec.message()); - return false; - } - if (!ssl_context.allow_any_cert && !verified) - { - MERROR("Peer did not provide a certificate in the allowed list, connection dropped"); + MERROR("SSL handshake failed, connection dropped: " << ec.message()); return false; } MDEBUG("SSL handshake success"); |