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(a)mariadb.org