Hi, Georg, On Jul 31, Georg Richter wrote:
revision-id: 1287c901 (v3.4.0-5-g1287c901) parent(s): 5386f1a3 author: Georg Richter committer: Georg Richter timestamp: 2024-07-16 13:12:26 +0200 message:
TLS/SSL changes (major rework)
diff --git a/include/ma_common.h b/include/ma_common.h index dfa96621..dc900b0d 100644 --- a/include/ma_common.h +++ b/include/ma_common.h @@ -131,15 +131,9 @@ typedef struct st_mariadb_field_extension MARIADB_CONST_STRING metadata[MARIADB_FIELD_ATTR_LAST+1]; /* 10.5 */ } MA_FIELD_EXTENSION;
-#if defined(HAVE_SCHANNEL) || defined(HAVE_GNUTLS) -#define reset_tls_self_signed_error(mysql) \ - do { \ - free((char*)mysql->net.tls_self_signed_error); \ - mysql->net.tls_self_signed_error= 0; \ - } while(0) -#else -#define reset_tls_self_signed_error(mysql) \ - do { \ - mysql->net.tls_self_signed_error= 0; \ +#ifdef HAVE_TLS +#define reset_tls_error(mysql) \ + do { \ + mysql->net.tls_verify_status= 0; \
you've lost the original error message when you've changed char* pointer to the message to my_bool flag.
} while(0) #endif diff --git a/include/ma_tls.h b/include/ma_tls.h index 23f53560..444ea4aa 100644 --- a/include/ma_tls.h +++ b/include/ma_tls.h @@ -134,6 +141,7 @@ const char *ma_tls_get_cipher(MARIADB_TLS *ssl); hash_type hash_type as defined in ma_hash.h fp buffer for fingerprint fp_len buffer length + my_bool verify_period
I don't see any 'my bool verify_period' in the list of arguments
Returns: actual size of finger print @@ -150,7 +158,7 @@ unsigned int ma_tls_get_finger_print(MARIADB_TLS *ctls, uint hash_type, char *fp int ma_tls_get_protocol_version(MARIADB_TLS *ctls); const char *ma_pvio_tls_get_protocol_version(MARIADB_TLS *ctls); int ma_pvio_tls_get_protocol_version_id(MARIADB_TLS *ctls); -unsigned int ma_tls_get_peer_cert_info(MARIADB_TLS *ctls); +unsigned int ma_tls_get_peer_cert_info(MARIADB_TLS *ctls, unsigned int size);
is this part of the public API?
void ma_tls_set_connection(MYSQL *mysql);
/* Function prototypes */ diff --git a/include/mysql.h b/include/mysql.h index 6c84a462..ba92527d 100644 --- a/include/mysql.h +++ b/include/mysql.h @@ -448,6 +449,16 @@ typedef struct st_mysql_time #define MYSQL_WAIT_EXCEPT 4 #define MYSQL_WAIT_TIMEOUT 8
+#define MARIADB_TLS_VERIFY_OK 0 +#define MARIADB_TLS_VERIFY_TRUST 1 +#define MARIADB_TLS_VERIFY_HOST 2
we've agreed to swap two previous lines
+#define MARIADB_TLS_VERIFY_PERIOD 4 +#define MARIADB_TLS_VERIFY_FINGERPRINT 8
is that right? you want to allow fingerprints even if the cert is expired?
+#define MARIADB_TLS_VERIFY_REVOKED 16 +#define MARIADB_TLS_VERIFY_UNKNOWN 32 +#define MARIADB_TLS_VERIFY_ERROR 128 /* last */ + + typedef struct character_set { unsigned int number; /* character set number */ diff --git a/libmariadb/ma_tls.c b/libmariadb/ma_tls.c index 1bda7dcf..f6ea6661 100644 --- a/libmariadb/ma_tls.c +++ b/libmariadb/ma_tls.c @@ -103,9 +103,54 @@ my_bool ma_pvio_tls_close(MARIADB_TLS *ctls) return ma_tls_close(ctls); }
-int ma_pvio_tls_verify_server_cert(MARIADB_TLS *ctls) +int ma_pvio_tls_verify_server_cert(MARIADB_TLS *ctls, unsigned int flags) { - return ma_tls_verify_server_cert(ctls); + MYSQL *mysql; + int rc; + + if (!ctls || !ctls->pvio || !ctls->pvio->mysql) + return 0; + + mysql= ctls->pvio->mysql; + + /* Skip peer certificate verification */ + if (ctls->pvio->mysql->options.extension->tls_allow_invalid_server_cert) + { + return 0; + } + + rc= ma_tls_verify_server_cert(ctls, flags); + + /* Set error messages */ + if (!mysql->net.last_errno) + { + if (mysql->net.tls_verify_status & MARIADB_TLS_VERIFY_PERIOD) + my_set_error(mysql, CR_SSL_CONNECTION_ERROR, SQLSTATE_UNKNOWN, + ER(CR_SSL_CONNECTION_ERROR), + "Certificate not yet valid or expired"); + else if (mysql->net.tls_verify_status & MARIADB_TLS_VERIFY_FINGERPRINT) + my_set_error(mysql, CR_SSL_CONNECTION_ERROR, SQLSTATE_UNKNOWN, + ER(CR_SSL_CONNECTION_ERROR), + "Fingerprint validation of peer certificate failed"); + else if (mysql->net.tls_verify_status & MARIADB_TLS_VERIFY_REVOKED) + my_set_error(mysql, CR_SSL_CONNECTION_ERROR, SQLSTATE_UNKNOWN, + ER(CR_SSL_CONNECTION_ERROR), + "Certificate revoked"); + else if (mysql->net.tls_verify_status & MARIADB_TLS_VERIFY_HOST) + my_set_error(mysql, CR_SSL_CONNECTION_ERROR, SQLSTATE_UNKNOWN, + ER(CR_SSL_CONNECTION_ERROR), + "Hostname verification failed"); + else if (mysql->net.tls_verify_status & MARIADB_TLS_VERIFY_UNKNOWN) + my_set_error(mysql, CR_SSL_CONNECTION_ERROR, SQLSTATE_UNKNOWN, + ER(CR_SSL_CONNECTION_ERROR), + "Peer certificate verification failed"); + else if (mysql->net.tls_verify_status & MARIADB_TLS_VERIFY_TRUST) + my_set_error(mysql, CR_SSL_CONNECTION_ERROR, SQLSTATE_UNKNOWN, + ER(CR_SSL_CONNECTION_ERROR), + "Peer certificate is not trusted");
let's try to preserve the error as set by the ssl library here
+ } + + return rc; }
const char *ma_pvio_tls_cipher(MARIADB_TLS *ctls) diff --git a/plugins/auth/my_auth.c b/plugins/auth/my_auth.c index faea968c..e7429fae 100644 --- a/plugins/auth/my_auth.c +++ b/plugins/auth/my_auth.c @@ -382,14 +419,30 @@ static int send_client_reply_packet(MCPVIO_EXT *mpvio, errno); goto error; } + mysql->net.tls_verify_status = 0; if (ma_pvio_start_ssl(mysql->net.pvio)) goto error; - if (mysql->net.tls_self_signed_error && - (!mysql->passwd || !mysql->passwd[0] || !hashing(mpvio->plugin))) - { - /* cannot use auth to validate the cert */ - set_error_from_tls_self_signed_error(mysql); - goto error; + + verify_flags= MARIADB_TLS_VERIFY_PERIOD | MARIADB_TLS_VERIFY_REVOKED; + if (have_fingerprint(mysql)) + { + verify_flags|= MARIADB_TLS_VERIFY_FINGERPRINT; + } else { + verify_flags|= MARIADB_TLS_VERIFY_TRUST | MARIADB_TLS_VERIFY_HOST; + } + + if (ma_pvio_tls_verify_server_cert(mysql->net.pvio->ctls, verify_flags) > MARIADB_TLS_VERIFY_OK) + { + if (mysql->net.tls_verify_status > MARIADB_TLS_VERIFY_TRUST || + (mysql->options.ssl_ca || mysql->options.ssl_capath)) + goto error; + + if (is_local_connection(mysql->net.pvio))
I'd say this should be earlier. Like if (!is_local_connection(mysql->net.pvio)) if (have_fingerprint(mysql)) { verify_flags|= MARIADB_TLS_VERIFY_FINGERPRINT; } else { verify_flags|= MARIADB_TLS_VERIFY_TRUST | MARIADB_TLS_VERIFY_HOST; }
+ { + CLEAR_CLIENT_ERROR(mysql); + } + else if (!password_and_hashing(mysql, mpvio->plugin)) + goto error; } } #endif /* HAVE_TLS */ @@ -769,8 +822,14 @@ retry: auth_plugin= &dummy_fallback_client_plugin;
/* can we use this plugin with this tls server cert ? */ - if (mysql->net.tls_self_signed_error && !hashing(auth_plugin)) - return set_error_from_tls_self_signed_error(mysql); + if ((mysql->net.tls_verify_status & MARIADB_TLS_VERIFY_TRUST) && + !password_and_hashing(mysql, auth_plugin)) + { + my_set_error(mysql, CR_SSL_CONNECTION_ERROR, SQLSTATE_UNKNOWN, + ER(CR_SSL_CONNECTION_ERROR), + "Certificate verification failure: The certificate is NOT trusted.");
already commented about using library error message
+ return 1; + } goto retry; } /* @@ -782,7 +841,9 @@ retry: if (ma_read_ok_packet(mysql, mysql->net.read_pos + 1, pkt_length)) return -1;
- if (!mysql->net.tls_self_signed_error) + if (!mysql->net.tls_verify_status || + ((mysql->net.tls_verify_status & MARIADB_TLS_VERIFY_TRUST) && + is_local_connection(mysql->net.pvio)))
Not needed, see above
return 0;
assert(mysql->options.use_ssl); diff --git a/libmariadb/secure/openssl.c b/libmariadb/secure/openssl.c index 8231a244..b5b573a9 100644 --- a/libmariadb/secure/openssl.c +++ b/libmariadb/secure/openssl.c @@ -459,24 +461,38 @@ error: return NULL; }
-unsigned int ma_tls_get_peer_cert_info(MARIADB_TLS *ctls) +unsigned int ma_tls_get_peer_cert_info(MARIADB_TLS *ctls, uint hash_size) { X509 *cert; + unsigned int hash_alg; SSL *ssl; + char fp[129];
+ switch (hash_size) { + case 0: + case 256: + hash_alg= MA_HASH_SHA256; + break; + case 384: + hash_alg= MA_HASH_SHA384; + break; + case 512: + hash_alg= MA_HASH_SHA512; + break; + default: + return 1; + } + if (!ctls || !ctls->ssl) return 1;
- /* Did we already read peer cert information ? */ - if (ctls->cert_info.version) - return 0; - ssl= (SSL *)ctls->ssl;
/* Store peer certificate information */ + if (!ctls->cert_info.version) + {
when can it be already set?
if ((cert= SSL_get_peer_certificate(ssl))) { - char fp[33]; #if OPENSSL_VERSION_NUMBER >= 0x10101000L const ASN1_TIME *not_before= X509_get0_notBefore(cert), *not_after= X509_get0_notAfter(cert); @@ -714,9 +714,40 @@ my_bool ma_tls_close(MARIADB_TLS *ctls) return rc; }
-int ma_tls_verify_server_cert(MARIADB_TLS *ctls) +/** Check for possible errors, and store the result in net.tls_verify_status. + verification will happen after handshake by ma_tls_verify_server_cert(). + To retrieve all errors, this callback function returns always true. + (By default OpenSSL stops verification after first error +*/ +static int ma_verification_callback(int preverify_ok __attribute__((unused)), X509_STORE_CTX *ctx) { - X509 *cert; + SSL *ssl; + + int x509_err= X509_STORE_CTX_get_error(ctx); + + if ((ssl = X509_STORE_CTX_get_ex_data(ctx, SSL_get_ex_data_X509_STORE_CTX_idx()))) + { + MYSQL *mysql= (MYSQL *)SSL_get_app_data(ssl); + + if ((x509_err == X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT || + x509_err == X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN)) + mysql->net.tls_verify_status|= MARIADB_TLS_VERIFY_TRUST; + else if (x509_err == X509_V_ERR_CERT_REVOKED) + mysql->net.tls_verify_status|= MARIADB_TLS_VERIFY_REVOKED; + else if (x509_err == X509_V_ERR_CERT_NOT_YET_VALID || + x509_err == X509_V_ERR_CERT_HAS_EXPIRED) + mysql->net.tls_verify_status|= MARIADB_TLS_VERIFY_PERIOD; + else if (x509_err != X509_V_OK) + mysql->net.tls_verify_status|= MARIADB_TLS_VERIFY_UNKNOWN;
and here you need to store the error message in mysql->net. for example, else if (x509_err == X509_V_ERR_CERT_NOT_YET_VALID || x509_err == X509_V_ERR_CERT_HAS_EXPIRED) { if (mysql->net.tls_verify_status < MARIADB_TLS_VERIFY_PERIOD) mysql->net.tls_verify_error= X509_verify_cert_error_string(x509_err); mysql->net.tls_verify_status|= MARIADB_TLS_VERIFY_PERIOD; } or else if (x509_err == X509_V_ERR_CERT_NOT_YET_VALID || x509_err == X509_V_ERR_CERT_HAS_EXPIRED) { if (mysql->net.tls_verify_status < MARIADB_TLS_VERIFY_PERIOD) my_set_error(mysql, CR_SSL_CONNECTION_ERROR, SQLSTATE_UNKNOWN, ER(CR_SSL_CONNECTION_ERROR), X509_verify_cert_error_string(x509_err)); mysql->net.tls_verify_status|= MARIADB_TLS_VERIFY_PERIOD; }
+ } + + /* continue verification */ + return 1; +} + +int ma_tls_verify_server_cert(MARIADB_TLS *ctls, unsigned int verify_flags) +{ + X509 *cert= NULL; MYSQL *mysql; SSL *ssl; MARIADB_PVIO *pvio; @@ -734,52 +765,97 @@ int ma_tls_verify_server_cert(MARIADB_TLS *ctls) mysql= (MYSQL *)SSL_get_app_data(ssl); pvio= mysql->net.pvio;
+ if (verify_flags & MARIADB_TLS_VERIFY_FINGERPRINT) + { + if (ma_pvio_tls_check_fp(ctls, mysql->options.extension->tls_fp, mysql->options.extension->tls_fp_list)) + { + mysql->net.tls_verify_status |= MARIADB_TLS_VERIFY_FINGERPRINT; + return 1; + } + + /* if certificates are valid and no revocation error occured, + we can return */ + if (!(mysql->net.tls_verify_status & MARIADB_TLS_VERIFY_PERIOD) && + !(mysql->net.tls_verify_status & MARIADB_TLS_VERIFY_REVOKED))
you can check this before trying the fingerprint
+ { + mysql->net.tls_verify_status= MARIADB_TLS_VERIFY_OK; + return 0; + } + } + + if (mysql->net.tls_verify_status & verify_flags) + { + return 1; + } + + if (verify_flags & MARIADB_TLS_VERIFY_HOST) + { if (!mysql->host) { pvio->set_error(mysql, CR_SSL_CONNECTION_ERROR, SQLSTATE_UNKNOWN, ER(CR_SSL_CONNECTION_ERROR), "Invalid (empty) hostname"); - return 1; + mysql->net.tls_verify_status|= MARIADB_TLS_VERIFY_HOST; + return MARIADB_TLS_VERIFY_ERROR; }
if (!(cert= SSL_get_peer_certificate(ssl))) { pvio->set_error(mysql, CR_SSL_CONNECTION_ERROR, SQLSTATE_UNKNOWN, ER(CR_SSL_CONNECTION_ERROR), "Unable to get server certificate"); - return 1; + mysql->net.tls_verify_status|= MARIADB_TLS_VERIFY_HOST;
not sure it's MARIADB_TLS_VERIFY_HOST, may be MARIADB_TLS_VERIFY_UNKNOWN ?
+ return MARIADB_TLS_VERIFY_ERROR; } + #ifdef HAVE_OPENSSL_CHECK_HOST if (X509_check_host(cert, mysql->host, strlen(mysql->host), 0, 0) != 1 && X509_check_ip_asc(cert, mysql->host, 0) != 1) + { + mysql->net.tls_verify_status|= MARIADB_TLS_VERIFY_HOST; goto error; + } #else x509sn= X509_get_subject_name(cert);
if ((cn_pos= X509_NAME_get_index_by_NID(x509sn, NID_commonName, -1)) < 0) + { + mysql->net.tls_verify_status|= MARIADB_TLS_VERIFY_HOST; goto error; + }
if (!(cn_entry= X509_NAME_get_entry(x509sn, cn_pos))) + { + mysql->net.tls_verify_status|= MARIADB_TLS_VERIFY_HOST; goto error; + }
if (!(cn_asn1 = X509_NAME_ENTRY_get_data(cn_entry))) + { + mysql->net.tls_verify_status|= MARIADB_TLS_VERIFY_HOST; goto error; + }
cn_str = (char *)ASN1_STRING_data(cn_asn1);
/* Make sure there is no embedded \0 in the CN */ if ((size_t)ASN1_STRING_length(cn_asn1) != strlen(cn_str)) + { + mysql->net.tls_verify_status|= MARIADB_TLS_VERIFY_HOST; goto error; + }
if (strcmp(cn_str, mysql->host)) + { + mysql->net.tls_verify_status|= MARIADB_TLS_VERIFY_HOST; goto error; + } #endif X509_free(cert); - + } return 0; error: + if (cert) X509_free(cert);
- pvio->set_error(mysql, CR_SSL_CONNECTION_ERROR, SQLSTATE_UNKNOWN, - ER(CR_SSL_CONNECTION_ERROR), "Validation of SSL server certificate failed"); return 1; }
Regards, Sergei Chief Architect, MariaDB Server and security@mariadb.org