LCOV - code coverage report
Current view: top level - clients/mapilib - connect_openssl.c (source / functions) Hit Total Coverage
Test: coverage.info Lines: 121 186 65.1 %
Date: 2024-10-07 21:21:43 Functions: 5 5 100.0 %

          Line data    Source code
       1             : /*
       2             :  * SPDX-License-Identifier: MPL-2.0
       3             :  *
       4             :  * This Source Code Form is subject to the terms of the Mozilla Public
       5             :  * License, v. 2.0.  If a copy of the MPL was not distributed with this
       6             :  * file, You can obtain one at http://mozilla.org/MPL/2.0/.
       7             :  *
       8             :  * Copyright 2024 MonetDB Foundation;
       9             :  * Copyright August 2008 - 2023 MonetDB B.V.;
      10             :  * Copyright 1997 - July 2008 CWI.
      11             :  */
      12             : 
      13             : // Request compatibility with OpenSSL 1.1.1.
      14             : // We need this for the hashing API.
      15             : #define OPENSSL_API_COMPAT 0x10100000L
      16             : 
      17             : #include "monetdb_config.h"
      18             : 
      19             : 
      20             : #include "stream.h"           /* include before mapi.h */
      21             : #include "stream_socket.h"
      22             : #include "mapi.h"
      23             : #include "mapi_intern.h"
      24             : 
      25             : #include <stdarg.h>
      26             : #include <openssl/ssl.h>
      27             : #include <openssl/err.h>
      28             : #include <openssl/x509v3.h>
      29             : 
      30             : MapiMsg
      31           6 : croak_openssl(Mapi mid, const char *action, const char *fmt, ...)
      32             : {
      33           6 :         va_list ap;
      34           6 :         char buffer[800];
      35           6 :         va_start(ap, fmt);
      36           6 :         vsnprintf(buffer, sizeof(buffer), fmt, ap);
      37           6 :         va_end(ap);
      38             : 
      39           6 :         unsigned long err = ERR_get_error();
      40          12 :         const char *errmsg =
      41             : #ifdef ERR_SYSTEM_ERROR
      42           6 :                 ERR_SYSTEM_ERROR(err)
      43           0 :                 ? strerror(ERR_GET_REASON(err))
      44           6 :                 :
      45             : #endif
      46           6 :                 ERR_reason_error_string(err);
      47           6 :         if (errmsg)
      48           6 :                 return mapi_printError(mid, action, MERROR, "TLS error: %s: %s", buffer, errmsg);
      49           0 :         else if (err != 0)
      50           0 :                 return mapi_printError(mid, action, MERROR, "TLS error: %s: failed with error %lu (0x%lx)", buffer, err, err);
      51             :         else
      52           0 :                 return mapi_printError(mid, action, MERROR, "TLS error: %s", buffer);
      53             : }
      54             : 
      55             : #ifndef NATIVE_WIN32
      56             : MapiMsg
      57           2 : add_system_certificates(Mapi mid, SSL_CTX *ctx)
      58             : {
      59             :         // On Linux, the Linux distribution has arranged for the system root certificates
      60             :         // to be found in OpenSSL's default locations.
      61             :         // On MacOS, MonetDB is generally installed using Homebrew and then Homebrew
      62             :         // takes care of it.
      63             :         // On Windows we use another implementation of add_system_certificates(),
      64             :         // found in openssl_windows.c.
      65             : 
      66           2 :         if (1 != SSL_CTX_set_default_verify_paths(ctx)) {
      67           0 :                 SSL_CTX_free(ctx);
      68           0 :                 return croak_openssl(mid, __func__, "SSL_CTX_set_default_verify_paths");
      69             :         }
      70             :         return MOK;
      71             : }
      72             : #endif
      73             : 
      74             : static MapiMsg
      75          12 : make_ssl_context(Mapi mid, SSL_CTX **ctx_out)
      76             : {
      77             :         // Today we just create a new one but if we load the system trust store
      78             :         // the result could be cached for a while.
      79             :         // (What's a reasonable amount of time for a process to pick up changes to
      80             :         // the system trust store?)
      81             : 
      82          12 :         MapiMsg msg;
      83          12 :         *ctx_out = NULL;
      84             : 
      85          12 :         const SSL_METHOD *method = TLS_method();
      86          12 :         if (!method)
      87           0 :                 return croak_openssl(mid, __func__, "TLS_method");
      88          12 :         SSL_CTX *ctx = SSL_CTX_new(method);
      89          12 :         if (!ctx)
      90           0 :                 return croak_openssl(mid, __func__, "SSL_CTX_new");
      91             :         // From here on we need to free 'ctx' on failure
      92             : 
      93          12 :         SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL);
      94          12 :         SSL_CTX_set_options(ctx, SSL_OP_NO_COMPRESSION);
      95          12 :         SSL_CTX_set_min_proto_version(ctx, TLS1_3_VERSION);
      96             :         // Because we use at least TLSv1.3 we don't need to mess with
      97             :         // SSL_CTX_set_cipher_list() and SSL_CTX_set_ciphersuites().
      98             : 
      99          12 :         const char *cert;
     100          12 :         switch (msettings_connect_tls_verify(mid->settings)) {
     101           3 :                 case verify_none:
     102             :                 case verify_hash:
     103           3 :                         SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, NULL);
     104           3 :                         break;
     105           7 :                 case verify_cert:
     106           7 :                         cert = msetting_string(mid->settings, MP_CERT);
     107           7 :                         if (1 != SSL_CTX_load_verify_locations(ctx, cert, NULL)) {
     108           0 :                                 SSL_CTX_free(ctx);
     109           0 :                                 return croak_openssl(mid, __func__, "SSL_CTX_load_verify_file: %s", cert);
     110             :                         }
     111             :                         break;
     112           2 :                 case verify_system:
     113           2 :                         msg = add_system_certificates(mid, ctx);
     114           2 :                         if (msg != MOK) {
     115           0 :                                 SSL_CTX_free(ctx);
     116           0 :                                 return msg;
     117             :                         }
     118             :                         break;
     119             :         }
     120             : 
     121          12 :         *ctx_out = ctx;
     122          12 :         return MOK;
     123             : }
     124             : 
     125             : static MapiMsg
     126           3 : verify_server_certificate_hash(Mapi mid, X509 *x509, const char *required_prefix)
     127             : {
     128           3 :         mapi_log_record(mid, "CONN", "verifying certificate hash against prefix '%s'", required_prefix);
     129             : 
     130           3 :         size_t prefix_len = strlen(required_prefix);
     131           3 :         if (prefix_len > 2 * SHA256_DIGEST_LENGTH)
     132           0 :                 return mapi_setError(mid, "value of certhash= is longer than a sha256 digest", __func__, MERROR);
     133             : 
     134             :         // Convert to DER
     135           3 :         unsigned char *buf = NULL;
     136           3 :         int buflen = i2d_X509(x509, &buf);
     137           3 :         if (buflen <= 0) {
     138           0 :                 return croak_openssl(mid, __func__, "could not convert server certificate to DER");
     139             :         }
     140           3 :         assert(buf);
     141             : 
     142             :         // Compute the has of the DER using the deprecated API so we stay
     143             :         // compatible with OpenSSL 1.1.1.
     144           3 :         SHA256_CTX sha256;
     145           3 :         if (1 != SHA256_Init(&sha256)) {
     146           0 :                 OPENSSL_free(buf);
     147           0 :                 return mapi_setError(mid, "SHA256_Init", __func__, MERROR);
     148             :         }
     149           3 :         if (1 != SHA256_Update(&sha256, buf, buflen)) {
     150           0 :                 OPENSSL_free(buf);
     151           0 :                 return mapi_setError(mid, "SHA256_Update", __func__, MERROR);
     152             :         }
     153           3 :         unsigned char digest[SHA256_DIGEST_LENGTH];
     154           3 :         if (1 != SHA256_Final(digest, &sha256)) {
     155           0 :                 OPENSSL_free(buf);
     156           0 :                 return mapi_setError(mid, "SHA256_Final", __func__, MERROR);
     157             :         }
     158           3 :         OPENSSL_free(buf);
     159             : 
     160             :         // Make hexadecimal;
     161           3 :         char hex[2 * SHA256_DIGEST_LENGTH + 1];
     162          99 :         for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) {
     163          96 :                 snprintf(&hex[2 * i], 3, "%02x", digest[i]);
     164             :         }
     165           3 :         assert(hex[2 * SHA256_DIGEST_LENGTH] == '\0');
     166             : 
     167             :         // Compare the digits
     168           3 :         if (strncmp(required_prefix, hex, prefix_len) != 0)
     169           2 :                 return mapi_setError(mid, "server certificate does not match certhash= prefix", __func__, MERROR);
     170             : 
     171           1 :         mapi_log_record(mid, "CONN", "server certificate matches certhash");
     172             :         return MOK;
     173             : }
     174             : MapiMsg
     175          12 : wrap_tls(Mapi mid, SOCKET sock)
     176             : {
     177             :         // Based on the example on the OpenSSL wiki:
     178             :         // https://wiki.openssl.org/index.php/SSL/TLS_Client
     179             : 
     180             :         // On error: close 'sock'.
     181             : 
     182          12 :         MapiMsg msg = MOK;
     183             : 
     184          12 :         const msettings *settings = mid->settings;
     185          12 :         const char *host = msettings_connect_tcp(settings);
     186          12 :         int port = msettings_connect_port(settings);
     187          12 :         size_t hostlen = strlen(host);
     188          12 :         size_t hostportlen = hostlen + 1 + 20;
     189             : 
     190          12 :         const char *clientkey = msettings_connect_clientkey(settings);
     191          12 :         const char *clientcert = msettings_connect_clientcert(settings);
     192          12 :         enum msetting_tls_verify verify_method = msettings_connect_tls_verify(settings);
     193             : 
     194             :         // Clear any earlier errors
     195          12 :         do {} while (ERR_get_error() != 0);
     196             : 
     197          12 :         SSL_CTX *ctx = NULL;
     198          12 :         msg = make_ssl_context(mid, &ctx);
     199          12 :         if (msg != MOK) {
     200           0 :                 closesocket(sock);
     201           0 :                 return msg;
     202             :         }
     203             :         // On error: close 'sock' and free 'ctx'.
     204             : 
     205             :         /////////////////////////////////////////////////////////////////////
     206             :         // Create the SSL connection
     207             : 
     208             :         // BIO 'bio' represents the whole SSL connection.
     209             :         // We will read and write plaintext from it.
     210          12 :         BIO *bio = BIO_new_ssl(ctx, 1);
     211          12 :         if (bio == NULL) {
     212           0 :                 closesocket(sock);
     213           0 :                 SSL_CTX_free(ctx);
     214           0 :                 return croak_openssl(mid, __func__, "BIO_new_ssl");
     215             :         }
     216             :         // BIO_new_ssl() inc'd the reference count of ctx so we can drop our
     217             :         // reference here.
     218          12 :         SSL_CTX_free(ctx);
     219             :         // On error: close 'sock' and free 'bio'
     220             : 
     221          12 :         SSL *ssl = NULL;
     222          12 :         if (1 != BIO_get_ssl(bio, &ssl)) {
     223           0 :                 closesocket(sock);
     224           0 :                 BIO_free(bio);
     225           0 :                 return croak_openssl(mid, __func__, "BIO_get_ssl");
     226             :         }
     227             :         // As far as I know the SSL returned by BIO_get_ssl has not had
     228             :         // its refcount inc'd so we don't need to free it.
     229             :         // On error: close 'sock' and free 'bio'.
     230          12 :         assert(ssl != NULL);
     231             : 
     232             :         // BIO 'sockbio' wraps the socket. OpenSSL will read and write
     233             :         // ciphertext from it.
     234             :         //
     235             :         // The OpenSSL developers apparently believe that it's safe to cast
     236             :         // SOCKET to int here, see https://github.com/openssl/openssl/blob/openssl-3.1.3/include/internal/sockets.h#L54-L58
     237          12 :         BIO *sockbio = BIO_new_socket((int)sock, BIO_CLOSE);
     238          12 :         if (sockbio == NULL) {
     239           0 :                 closesocket(sock);
     240           0 :                 BIO_free_all(bio);
     241           0 :                 return croak_openssl(mid, __func__, "BIO_new_socket");
     242             :         }
     243             :         // From here on, 'sock' will be free'd by 'sockbio'.
     244             :         // On error: free 'sockbio' and free 'bio'.
     245             : 
     246          12 :         if (!BIO_up_ref(sockbio)) {
     247           0 :                 BIO_free_all(sockbio);
     248           0 :                 BIO_free_all(bio);
     249           0 :                 return croak_openssl(mid, __func__, "BIO_up_ref sockbio");
     250             :         }
     251          12 :         SSL_set0_rbio(ssl, sockbio); // consumes first ref
     252          12 :         SSL_set0_wbio(ssl, sockbio); // consumes second ref
     253             :         // from here on 'sockbio' will be freed through 'ssl' which is freed through 'bio'.
     254             :         // On error: free 'bio'.
     255             : 
     256          12 :         if (!SSL_set_tlsext_host_name(ssl, host)) {
     257           0 :                 BIO_free_all(bio);
     258           0 :                 return croak_openssl(mid, __func__, "SSL_set_tlsext_host_name");
     259             :         }
     260             : 
     261          12 :         X509_VERIFY_PARAM *param = SSL_get0_param(ssl);
     262          12 :         if (param == NULL) {
     263           0 :                 BIO_free_all(bio);
     264           0 :                 return croak_openssl(mid, __func__, "SSL_get0_param");
     265             :         }
     266          12 :         X509_VERIFY_PARAM_set_hostflags(param, X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS);
     267          12 :         if (1 != X509_VERIFY_PARAM_set1_host(param, host, strlen(host))) {
     268           0 :                 BIO_free_all(bio);
     269           0 :                 return croak_openssl(mid, __func__, "X509_VERIFY_PARAM_set1_host");
     270             :         }
     271             : 
     272             :         // Temporarily disable the ALPN header.
     273             :         // TODO re-enable it when test systemcertificates.py no longer relies
     274             :         // on connecting to an HTTPS server. (Which is an ugly hack in the first place!)
     275             : #if 0
     276             :         unsigned char alpn_vector[] = { 6, 'm', 'a', 'p', 'i', '/', '9' };
     277             :         // NOTE: these functions return 0 on success, not 1!
     278             :         if (0 != SSL_set_alpn_protos(ssl, alpn_vector, sizeof(alpn_vector))) {
     279             :                 BIO_free_all(bio);
     280             :                 return croak_openssl(mid, __func__, "SSL_set_alpn_protos");
     281             :         }
     282             : #endif
     283             : 
     284          12 :         assert(clientkey);
     285          12 :         assert(clientcert);
     286          12 :         if (clientkey[0]) {
     287           1 :                 if (1 != SSL_use_PrivateKey_file(ssl, clientkey, SSL_FILETYPE_PEM)) {
     288           0 :                         BIO_free_all(bio);
     289           0 :                         return croak_openssl(mid, __func__, "SSL_use_PrivateKey_file");
     290             :                 }
     291           1 :                 if (1 != SSL_use_certificate_chain_file(ssl, clientcert)) {
     292           0 :                         BIO_free_all(bio);
     293           0 :                         return croak_openssl(mid, __func__, "SSL_use_certificate_chain_file");
     294             :                 }
     295             :         }
     296             : 
     297             :         // Handshake.
     298          12 :         if (1 != SSL_connect(ssl)) {
     299           6 :                 BIO_free_all(bio);
     300           6 :                 return croak_openssl(mid, __func__, "SSL_connect handshake");
     301             :         }
     302             : 
     303             :         // Verify the server certificate
     304           6 :         X509 *server_cert = SSL_get_peer_certificate(ssl);
     305           6 :         if (server_cert == NULL) {
     306           0 :                 BIO_free_all(bio);
     307           0 :                 return croak_openssl(mid, __func__, "Server did not send a certificate");
     308             :         }
     309             :         // be careful when to free server_cert
     310           6 :         if (verify_method == verify_hash) {
     311           3 :                 const char *required_prefix = msettings_connect_certhash_digits(settings);
     312           3 :                 msg = verify_server_certificate_hash(mid, server_cert, required_prefix);
     313           3 :                 X509_free(server_cert);
     314           3 :                 if (msg != MOK) {
     315           2 :                         BIO_free_all(bio);
     316           2 :                         return msg;
     317             :                 }
     318             :         } else {
     319           3 :                 X509_free(server_cert);
     320           3 :                 long verify_result = SSL_get_verify_result(ssl);
     321           3 :                 if (verify_result != X509_V_OK) {
     322           0 :                         BIO_free_all(bio);
     323           0 :                         const char *error_message = X509_verify_cert_error_string(verify_result);
     324           0 :                         return croak_openssl(mid, __func__, "Invalid server certificate: %s", error_message);
     325             :                 }
     326             :         }
     327             : 
     328             :         /////////////////////////////////////////////////////////////////////
     329             :         // Attach the connection to 'mid'
     330             : 
     331           4 :         if (!BIO_up_ref(bio)) {
     332           0 :                 BIO_free_all(bio);
     333           0 :                 return croak_openssl(mid, __func__, "BIO_up_ref bio");
     334             :         }
     335             :         // On error: free 'bio' twice
     336             : 
     337           4 :         char *hostcolonport = malloc(hostportlen);
     338           4 :         if (hostcolonport != NULL)
     339           4 :                 snprintf(hostcolonport, hostportlen, "%s:%d", host, port);
     340             : 
     341           4 :         stream *rstream = openssl_rstream(hostcolonport ? hostcolonport : "ssl rstream", bio);
     342           4 :         if (rstream == NULL || mnstr_errnr(rstream) != MNSTR_NO__ERROR) {
     343           0 :                 BIO_free_all(bio); // drops first ref
     344           0 :                 BIO_free_all(bio); // drops second ref
     345           0 :                 free(hostcolonport);
     346           0 :                 msg = croak_openssl(mid, __func__, "openssl_rstream: %s", mnstr_peek_error(rstream));
     347           0 :                 close_stream(rstream);
     348           0 :                 return msg;
     349             :         }
     350             :         // On error: free 'bio' and close 'rstream'.
     351           8 :         stream *wstream = openssl_wstream(hostcolonport ? hostcolonport : "ssl wstream", bio);
     352           4 :         free(hostcolonport);
     353           4 :         if (wstream == NULL || mnstr_errnr(wstream) != MNSTR_NO__ERROR) {
     354           0 :                 BIO_free_all(bio);
     355           0 :                 close_stream(rstream);
     356           0 :                 msg = croak_openssl(mid, __func__, "openssl_wstream: %s", mnstr_peek_error(wstream));
     357           0 :                 close_stream(wstream);
     358           0 :                 return msg;
     359             :         }
     360             :         // On error: free 'rstream' and 'wstream'.
     361           4 :         msg = mapi_wrap_streams(mid, rstream, wstream);
     362           4 :         if (msg != MOK) {
     363           0 :                 close_stream(rstream);
     364           0 :                 close_stream(wstream);
     365           0 :                 return msg;
     366             :         }
     367             :         // 'rstream' and 'wstream' are part of 'mid' now.
     368             : 
     369             : 
     370           4 :         mapi_log_record(mid, "CONN", "TLS handshake succeeded");
     371             :         return MOK;
     372             : }

Generated by: LCOV version 1.14