/* SPDX-License-Identifier: MIT OR GPL-3.0-only */ /* tls.c ** strophe XMPP client library -- TLS abstraction header ** ** Copyright (C) 2005-2009 Collecta, Inc. ** ** This software is provided AS-IS with no warranty, either express ** or implied. ** ** This program is dual licensed under the MIT or GPLv3 licenses. */ /** @file * TLS implementation with GNUTLS */ #include #include #include #include #include #include #include "common.h" #include "tls.h" #include "sock.h" #if GNUTLS_VERSION_NUMBER < 0x030702 /* This is a very dirty workaround to make this file compile on older GnuTLS * We simply define our own value for the later introduced * `GNUTLS_CB_TLS_EXPORTER` which makes it compile, but it will fail on runtime * when the combination of the following options happen: * * GnuTLS < 3.7.2 * * TLS 1.3 * * A SCRAM PLUS variant to authenticate * We will think about fixing this once a user pops up who has that problem. */ #define GNUTLS_CB_TLS_EXPORTER 3 #endif struct _tls { xmpp_ctx_t *ctx; /* do we need this? */ xmpp_conn_t *conn; gnutls_session_t session; gnutls_certificate_credentials_t cred; gnutls_x509_crt_t client_cert; gnutls_datum_t channel_binding; int lasterror; }; void tls_initialize(void) { /* initialize the GNU TLS global state */ gnutls_global_init(); /* TODO: wire in xmpp_ctx_t allocator somehow? unfortunately in gnutls it's global, so we can only do so much. */ } void tls_shutdown(void) { /* tear down the GNU TLS global state */ gnutls_global_deinit(); } static int _tls_password_callback(void *userdata, int attempt, const char *token_url, const char *token_label, unsigned int flags, char *pin, size_t pin_max) { xmpp_conn_t *conn = userdata; UNUSED(attempt); UNUSED(token_url); UNUSED(token_label); UNUSED(flags); int ret = tls_caching_password_callback(pin, pin_max, conn); return ret > 0 ? 0 : GNUTLS_E_PKCS11_PIN_ERROR; } static gnutls_x509_crt_t _tls_load_cert_x509(xmpp_conn_t *conn) { gnutls_x509_crt_t cert; gnutls_datum_t data; int res; if (gnutls_x509_crt_init(&cert) < 0) return NULL; if (gnutls_load_file(conn->tls_client_cert, &data) < 0) goto error_out; res = gnutls_x509_crt_import(cert, &data, GNUTLS_X509_FMT_PEM); gnutls_free(data.data); if (res < 0) goto error_out; return cert; error_out: gnutls_x509_crt_deinit(cert); return NULL; } static gnutls_x509_crt_t _tls_load_cert_p12(xmpp_conn_t *conn) { gnutls_pkcs12_t p12; gnutls_x509_crt_t *cert = NULL; gnutls_datum_t data; gnutls_x509_privkey_t key; unsigned int cert_num = 0, retries = 0; int err = -1; if (gnutls_pkcs12_init(&p12) < 0) return NULL; if (gnutls_load_file(conn->tls_client_cert, &data) < 0) goto error_out; if (gnutls_pkcs12_import(p12, &data, GNUTLS_X509_FMT_DER, 0) < 0) goto error_out2; /* First try to open file with no pass */ if ((err = gnutls_pkcs12_simple_parse(p12, NULL, &key, &cert, &cert_num, NULL, NULL, NULL, 0)) == 0) { goto done; } /* Now let's try to open file with an empty pass */ if ((err = gnutls_pkcs12_simple_parse(p12, "", &key, &cert, &cert_num, NULL, NULL, NULL, 0)) == 0) { goto done; } if (!conn->password_callback) { strophe_error(conn->ctx, "tls", "No password callback set"); goto error_out2; } /* ... and only now ask the user for a password */ while (retries++ < conn->password_retries) { char pass[GNUTLS_PKCS11_MAX_PIN_LEN]; int passlen = _tls_password_callback(conn, 0, NULL, NULL, 0, pass, sizeof(pass)); if (passlen < 0) continue; err = gnutls_pkcs12_simple_parse(p12, pass, &key, &cert, &cert_num, NULL, NULL, NULL, 0); memset(pass, 0, sizeof(pass)); if (err == 0) break; tls_clear_password_cache(conn); if (err != GNUTLS_E_DECRYPTION_FAILED && err != GNUTLS_E_MAC_VERIFY_FAILED) { strophe_error(conn->ctx, "tls", "could not read P12 file"); break; } strophe_debug(conn->ctx, "tls", "wrong password?"); } done: gnutls_pkcs12_deinit(p12); gnutls_free(data.data); if (err < 0) goto error_out; gnutls_x509_privkey_deinit(key); if (cert_num > 1) { strophe_error(conn->ctx, "tls", "Can't handle stack of %u certs", cert_num); goto error_out; } gnutls_x509_crt_t ret = *cert; gnutls_free(cert); return ret; error_out2: gnutls_free(data.data); error_out: tls_clear_password_cache(conn); if (cert) { for (unsigned int n = 0; n < cert_num; ++n) { gnutls_x509_crt_deinit(cert[n]); } gnutls_free(cert); } return NULL; } static gnutls_x509_crt_t _tls_load_cert(xmpp_conn_t *conn) { if (conn->tls && conn->tls->client_cert) return conn->tls->client_cert; if (conn->tls_client_cert && !conn->tls_client_key) { return _tls_load_cert_p12(conn); } return _tls_load_cert_x509(conn); } static void _tls_free_cert(xmpp_conn_t *conn, gnutls_x509_crt_t cert) { if (conn->tls && conn->tls->client_cert == cert) return; gnutls_x509_crt_deinit(cert); } static int _tls_get_id_on_xmppaddr(xmpp_conn_t *conn, gnutls_x509_crt_t cert, unsigned int n, char **ret) { gnutls_datum_t san; size_t name_len, oid_len; char oid[128], name[128]; name_len = oid_len = 128; int res = gnutls_x509_crt_get_subject_alt_name(cert, n, name, &name_len, NULL); if (res == GNUTLS_SAN_OTHERNAME_XMPP) { /* This is the happy flow path with fixed GnuTLS. * While implementing this I stumbled over an issue in GnuTLS * which lead to * https://gitlab.com/gnutls/gnutls/-/merge_requests/1397 */ if (ret) { *ret = strophe_strdup(conn->ctx, name); } return GNUTLS_SAN_OTHERNAME_XMPP; } if (res == GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE) return GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE; if (res != GNUTLS_SAN_OTHERNAME) return GNUTLS_E_X509_UNKNOWN_SAN; res = gnutls_x509_crt_get_subject_alt_othername_oid(cert, n, oid, &oid_len); if (res == GNUTLS_SAN_OTHERNAME_XMPP) { gnutls_datum_t xmpp_addr; san.data = (unsigned char *)name; san.size = name_len; res = gnutls_x509_othername_to_virtual(oid, &san, NULL, &xmpp_addr); if (res < 0) { gnutls_free(xmpp_addr.data); return GNUTLS_E_MEMORY_ERROR; } if (ret) { *ret = strophe_strdup(conn->ctx, (char *)xmpp_addr.data); } gnutls_free(xmpp_addr.data); return GNUTLS_SAN_OTHERNAME_XMPP; } return GNUTLS_E_X509_UNKNOWN_SAN; } static int _tls_id_on_xmppaddr(xmpp_conn_t *conn, gnutls_x509_crt_t cert, unsigned int n, char **ret) { int res = GNUTLS_E_SUCCESS; unsigned int i, j; for (i = j = 0; res != GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE; ++i) { res = _tls_get_id_on_xmppaddr(conn, cert, i, NULL); if (res == GNUTLS_SAN_OTHERNAME_XMPP) { if (j == n) { res = _tls_get_id_on_xmppaddr(conn, cert, i, ret); break; } j++; } } return res; } /** Search through the SubjectAlternativeNames and return the next * id-on-xmppAddr element starting from `n`. */ char *tls_id_on_xmppaddr(xmpp_conn_t *conn, unsigned int n) { char *ret = NULL; gnutls_x509_crt_t cert = _tls_load_cert(conn); if (cert == NULL) return NULL; _tls_id_on_xmppaddr(conn, cert, n, &ret); _tls_free_cert(conn, cert); return ret; } unsigned int tls_id_on_xmppaddr_num(xmpp_conn_t *conn) { unsigned int ret = 0, n; int res = GNUTLS_E_SUCCESS; gnutls_x509_crt_t cert = _tls_load_cert(conn); if (cert == NULL) return 0; for (n = 0; res != GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE; ++n) { res = _tls_id_on_xmppaddr(conn, cert, n, NULL); if (res == GNUTLS_SAN_OTHERNAME_XMPP) ret++; } _tls_free_cert(conn, cert); return ret; } static xmpp_tlscert_t *_x509_to_tlscert(xmpp_ctx_t *ctx, gnutls_x509_crt_t cert) { int res; char buf[512], smallbuf[64]; size_t size, m; unsigned int algo, n; gnutls_datum_t data; time_t time_val; xmpp_tlscert_t *tlscert = tlscert_new(ctx); gnutls_x509_crt_export2(cert, GNUTLS_X509_FMT_PEM, &data); tlscert->pem = strophe_alloc(ctx, data.size + 1); memcpy(tlscert->pem, data.data, data.size); tlscert->pem[data.size] = '\0'; gnutls_free(data.data); size = sizeof(buf); gnutls_x509_crt_get_dn(cert, buf, &size); tlscert->elements[XMPP_CERT_SUBJECT] = strophe_strdup(ctx, buf); size = sizeof(buf); gnutls_x509_crt_get_issuer_dn(cert, buf, &size); tlscert->elements[XMPP_CERT_ISSUER] = strophe_strdup(ctx, buf); time_val = gnutls_x509_crt_get_activation_time(cert); tlscert->elements[XMPP_CERT_NOTBEFORE] = strophe_strdup(ctx, ctime(&time_val)); tlscert->elements[XMPP_CERT_NOTBEFORE] [strlen(tlscert->elements[XMPP_CERT_NOTBEFORE]) - 1] = '\0'; time_val = gnutls_x509_crt_get_expiration_time(cert); tlscert->elements[XMPP_CERT_NOTAFTER] = strophe_strdup(ctx, ctime(&time_val)); tlscert->elements[XMPP_CERT_NOTAFTER] [strlen(tlscert->elements[XMPP_CERT_NOTAFTER]) - 1] = '\0'; size = sizeof(smallbuf); gnutls_x509_crt_get_fingerprint(cert, GNUTLS_DIG_SHA1, smallbuf, &size); hex_encode(buf, smallbuf, size); tlscert->elements[XMPP_CERT_FINGERPRINT_SHA1] = strophe_strdup(ctx, buf); size = sizeof(smallbuf); gnutls_x509_crt_get_fingerprint(cert, GNUTLS_DIG_SHA256, smallbuf, &size); hex_encode(buf, smallbuf, size); tlscert->elements[XMPP_CERT_FINGERPRINT_SHA256] = strophe_strdup(ctx, buf); strophe_snprintf(buf, sizeof(buf), "%d", gnutls_x509_crt_get_version(cert)); tlscert->elements[XMPP_CERT_VERSION] = strophe_strdup(ctx, buf); algo = gnutls_x509_crt_get_pk_algorithm(cert, NULL); tlscert->elements[XMPP_CERT_KEYALG] = strophe_strdup(ctx, gnutls_pk_algorithm_get_name(algo)); algo = gnutls_x509_crt_get_signature_algorithm(cert); tlscert->elements[XMPP_CERT_SIGALG] = strophe_strdup(ctx, gnutls_sign_get_name(algo)); size = sizeof(smallbuf); gnutls_x509_crt_get_serial(cert, smallbuf, &size); hex_encode(buf, smallbuf, size); tlscert->elements[XMPP_CERT_SERIALNUMBER] = strophe_strdup(ctx, buf); for (n = 0, m = 0, res = 0; res != GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE; ++n) { size = sizeof(buf); res = gnutls_x509_crt_get_subject_alt_name(cert, n, buf, &size, NULL); if (res == GNUTLS_SAN_DNSNAME) { if (tlscert_add_dnsname(tlscert, buf)) strophe_debug(ctx, "tls", "Can't store dnsName(%zu): %s", m, buf); m++; } } return tlscert; } static int _tls_verify(gnutls_session_t session) { tls_t *tls = gnutls_session_get_ptr(session); const gnutls_datum_t *cert_list; gnutls_certificate_type_t type; gnutls_datum_t out; unsigned int cert_list_size = 0, status; gnutls_x509_crt_t cert; if (gnutls_certificate_type_get(session) != GNUTLS_CRT_X509) return -1; if (gnutls_certificate_verify_peers2(session, &status) < 0) { strophe_error(tls->ctx, "tls", "Verify peers failed"); return -1; } type = gnutls_certificate_type_get(session); if (gnutls_certificate_verification_status_print(status, type, &out, 0) < 0) { strophe_error(tls->ctx, "tls", "Status print failed"); return -1; } /* Return early if the Certificate is trusted * OR if we trust all Certificates */ if (status == 0 || tls->conn->tls_trust) { gnutls_free(out.data); return 0; } if (!tls->conn->certfail_handler) { strophe_error(tls->ctx, "tls", "No certfail handler set, canceling connection attempt"); gnutls_free(out.data); return -1; } cert_list = gnutls_certificate_get_peers(session, &cert_list_size); /* OpenSSL displays the certificate chain in reverse order than GnuTLS. * To show consistent behavior to the user, traverse the list from the * end. */ while (cert_list_size--) { gnutls_x509_crt_init(&cert); gnutls_x509_crt_import(cert, &cert_list[cert_list_size], GNUTLS_X509_FMT_DER); xmpp_tlscert_t *tlscert = _x509_to_tlscert(tls->ctx, cert); if (!tlscert) { gnutls_x509_crt_deinit(cert); gnutls_free(out.data); return -1; } if (tls->conn->certfail_handler(tlscert, (char *)out.data) == 0) { xmpp_tlscert_free(tlscert); gnutls_x509_crt_deinit(cert); gnutls_free(out.data); return -1; } xmpp_tlscert_free(tlscert); gnutls_x509_crt_deinit(cert); } gnutls_free(out.data); return 0; } tls_t *tls_new(xmpp_conn_t *conn) { tls_t *tls = strophe_alloc(conn->ctx, sizeof(tls_t)); if (tls) { memset(tls, 0, sizeof(*tls)); tls->ctx = conn->ctx; tls->conn = conn; gnutls_init(&tls->session, GNUTLS_CLIENT); gnutls_certificate_allocate_credentials(&tls->cred); tls_set_credentials(tls, NULL); if (conn->password_callback) gnutls_certificate_set_pin_function(tls->cred, _tls_password_callback, conn); if (conn->tls_client_cert && conn->tls_client_key) { unsigned int retries = 0; tls->client_cert = _tls_load_cert(conn); if (!tls->client_cert) { strophe_error(tls->ctx, "tls", "could not read client certificate"); goto error_out; } while (retries++ < conn->password_retries) { int err = gnutls_certificate_set_x509_key_file( tls->cred, conn->tls_client_cert, conn->tls_client_key, GNUTLS_X509_FMT_PEM); if (err == 0) break; tls_clear_password_cache(conn); if (err != GNUTLS_E_DECRYPTION_FAILED) { strophe_error(tls->ctx, "tls", "could not read private key"); goto error_out; } strophe_debug(tls->ctx, "tls", "wrong password?"); } } else if (conn->tls_client_cert) { unsigned int retries = 0; while (retries++ < conn->password_retries) { char pass[GNUTLS_PKCS11_MAX_PIN_LEN]; pass[0] = '\0'; int passlen = _tls_password_callback(conn, 0, NULL, NULL, 0, pass, sizeof(pass)); if (passlen < 0) continue; int err = gnutls_certificate_set_x509_simple_pkcs12_file( tls->cred, conn->tls_client_cert, GNUTLS_X509_FMT_DER, pass); memset(pass, 0, sizeof(pass)); if (err == 0) break; tls_clear_password_cache(conn); if (err != GNUTLS_E_DECRYPTION_FAILED && err != GNUTLS_E_MAC_VERIFY_FAILED) { strophe_error(tls->ctx, "tls", "could not read P12 file"); goto error_out; } strophe_debug(tls->ctx, "tls", "wrong password?"); } } gnutls_certificate_set_verify_function(tls->cred, _tls_verify); gnutls_set_default_priority(tls->session); gnutls_session_set_ptr(tls->session, tls); /* fixme: this may require setting a callback on win32? */ gnutls_transport_set_int(tls->session, conn->sock); } return tls; error_out: if (tls->client_cert) gnutls_x509_crt_deinit(tls->client_cert); gnutls_certificate_free_credentials(tls->cred); gnutls_deinit(tls->session); strophe_free(tls->ctx, tls); return NULL; } void tls_free(tls_t *tls) { gnutls_free(tls->channel_binding.data); gnutls_x509_crt_deinit(tls->client_cert); gnutls_deinit(tls->session); gnutls_certificate_free_credentials(tls->cred); strophe_free(tls->ctx, tls); } xmpp_tlscert_t *tls_peer_cert(xmpp_conn_t *conn) { xmpp_tlscert_t *tlscert = NULL; if (conn && conn->tls && conn->tls->session) { unsigned int list_size = 0; const gnutls_datum_t *der_cert = gnutls_certificate_get_peers(conn->tls->session, &list_size); if (der_cert && list_size) { gnutls_x509_crt_t cert; if (gnutls_x509_crt_init(&cert) < 0) return NULL; if (gnutls_x509_crt_import(cert, der_cert, GNUTLS_X509_FMT_DER) == 0) tlscert = _x509_to_tlscert(conn->ctx, cert); gnutls_x509_crt_deinit(cert); } } return tlscert; } int tls_set_credentials(tls_t *tls, const char *cafilename) { UNUSED(cafilename); /* set trusted credentials -- takes a .pem filename */ int err = gnutls_certificate_set_x509_system_trust(tls->cred); if (err >= 0 && tls->conn->tls_cafile) err = gnutls_certificate_set_x509_trust_file( tls->cred, tls->conn->tls_cafile, GNUTLS_X509_FMT_PEM); if (err >= 0 && tls->conn->tls_capath) err = gnutls_certificate_set_x509_trust_dir( tls->cred, tls->conn->tls_capath, GNUTLS_X509_FMT_PEM); if (err >= 0) { err = gnutls_credentials_set(tls->session, GNUTLS_CRD_CERTIFICATE, tls->cred); } tls->lasterror = err; return err == GNUTLS_E_SUCCESS; } int tls_init_channel_binding(tls_t *tls, const char **binding_prefix, size_t *binding_prefix_len) { gnutls_channel_binding_t binding_type; gnutls_protocol_t tls_version = gnutls_protocol_get_version(tls->session); switch (tls_version) { case GNUTLS_SSL3: case GNUTLS_TLS1_0: case GNUTLS_TLS1_1: case GNUTLS_TLS1_2: *binding_prefix = "tls-unique"; *binding_prefix_len = strlen("tls-unique"); binding_type = GNUTLS_CB_TLS_UNIQUE; break; case GNUTLS_TLS1_3: *binding_prefix = "tls-exporter"; *binding_prefix_len = strlen("tls-exporter"); binding_type = GNUTLS_CB_TLS_EXPORTER; break; default: strophe_error(tls->ctx, "tls", "Unsupported TLS Version: %s", gnutls_protocol_get_name(tls_version)); return -1; } if (tls->channel_binding.data) { gnutls_free(tls->channel_binding.data); tls->channel_binding.data = NULL; } int ret = gnutls_session_channel_binding(tls->session, binding_type, &tls->channel_binding); if (ret) { strophe_error(tls->ctx, "tls", "could not get channel binding: %s", gnutls_strerror(ret)); } return ret; } const void *tls_get_channel_binding_data(tls_t *tls, size_t *size) { if (!tls->channel_binding.data || !tls->channel_binding.size) { strophe_error(tls->ctx, "tls", "No channel binding data available"); } *size = tls->channel_binding.size; return tls->channel_binding.data; } int tls_start(tls_t *tls) { sock_set_blocking(tls->conn->sock); tls->lasterror = gnutls_handshake(tls->session); sock_set_nonblocking(tls->conn->sock); return tls->lasterror == GNUTLS_E_SUCCESS; } int tls_stop(tls_t *tls) { tls->lasterror = gnutls_bye(tls->session, GNUTLS_SHUT_RDWR); return tls->lasterror == GNUTLS_E_SUCCESS; } int tls_error(struct conn_interface *intf) { return intf->conn->tls->lasterror; } int tls_is_recoverable(struct conn_interface *intf, int error) { UNUSED(intf); return !gnutls_error_is_fatal(error); } int tls_pending(struct conn_interface *intf) { return gnutls_record_check_pending(intf->conn->tls->session); } int tls_read(struct conn_interface *intf, void *buff, size_t len) { int ret; tls_t *tls = intf->conn->tls; ret = gnutls_record_recv(tls->session, buff, len); tls->lasterror = ret < 0 ? ret : 0; return ret; } int tls_write(struct conn_interface *intf, const void *buff, size_t len) { int ret; tls_t *tls = intf->conn->tls; ret = gnutls_record_send(tls->session, buff, len); tls->lasterror = ret < 0 ? ret : 0; return ret; } int tls_clear_pending_write(struct conn_interface *intf) { UNUSED(intf); return 0; }