diff options
Diffstat (limited to 'libfreerdp/crypto/tls.c')
-rw-r--r-- | libfreerdp/crypto/tls.c | 1887 |
1 files changed, 1887 insertions, 0 deletions
diff --git a/libfreerdp/crypto/tls.c b/libfreerdp/crypto/tls.c new file mode 100644 index 0000000..371b81b --- /dev/null +++ b/libfreerdp/crypto/tls.c @@ -0,0 +1,1887 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Transport Layer Security + * + * Copyright 2011-2012 Marc-Andre Moreau <marcandre.moreau@gmail.com> + * Copyright 2023 Armin Novak <anovak@thincast.com> + * Copyright 2023 Thincast Technologies GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <freerdp/config.h> + +#include "../core/settings.h" + +#include <winpr/assert.h> +#include <string.h> +#include <errno.h> + +#include <winpr/crt.h> +#include <winpr/string.h> +#include <winpr/sspi.h> +#include <winpr/ssl.h> + +#include <winpr/stream.h> +#include <freerdp/utils/ringbuffer.h> + +#include <freerdp/crypto/certificate.h> +#include <freerdp/crypto/certificate_data.h> + +#include <freerdp/log.h> +#include "../crypto/tls.h" +#include "../core/tcp.h" + +#include "opensslcompat.h" +#include "certificate.h" +#include "privatekey.h" + +#ifdef WINPR_HAVE_POLL_H +#include <poll.h> +#endif + +#ifdef FREERDP_HAVE_VALGRIND_MEMCHECK_H +#include <valgrind/memcheck.h> +#endif + +#define TAG FREERDP_TAG("crypto") + +/** + * Earlier Microsoft iOS RDP clients have sent a null or even double null + * terminated hostname in the SNI TLS extension. + * If the length indicator does not equal the hostname strlen OpenSSL + * will abort (see openssl:ssl/t1_lib.c). + * Here is a tcpdump segment of Microsoft Remote Desktop Client Version + * 8.1.7 running on an iPhone 4 with iOS 7.1.2 showing the transmitted + * SNI hostname TLV blob when connection to server "abcd": + * 00 name_type 0x00 (host_name) + * 00 06 length_in_bytes 0x0006 + * 61 62 63 64 00 00 host_name "abcd\0\0" + * + * Currently the only (runtime) workaround is setting an openssl tls + * extension debug callback that sets the SSL context's servername_done + * to 1 which effectively disables the parsing of that extension type. + * + * Nowadays this workaround is not required anymore but still can be + * activated by adding the following define: + * + * #define MICROSOFT_IOS_SNI_BUG + */ + +typedef struct +{ + SSL* ssl; + CRITICAL_SECTION lock; +} BIO_RDP_TLS; + +static int tls_verify_certificate(rdpTls* tls, const rdpCertificate* cert, const char* hostname, + UINT16 port); +static void tls_print_certificate_name_mismatch_error(const char* hostname, UINT16 port, + const char* common_name, char** alt_names, + size_t alt_names_count); +static void tls_print_new_certificate_warn(rdpCertificateStore* store, const char* hostname, + UINT16 port, const char* fingerprint); +static void tls_print_certificate_error(rdpCertificateStore* store, rdpCertificateData* stored_data, + const char* hostname, UINT16 port, const char* fingerprint); + +static void free_tls_public_key(rdpTls* tls) +{ + WINPR_ASSERT(tls); + free(tls->PublicKey); + tls->PublicKey = NULL; + tls->PublicKeyLength = 0; +} + +static void free_tls_bindings(rdpTls* tls) +{ + WINPR_ASSERT(tls); + + if (tls->Bindings) + free(tls->Bindings->Bindings); + + free(tls->Bindings); + tls->Bindings = NULL; +} + +static int bio_rdp_tls_write(BIO* bio, const char* buf, int size) +{ + int error = 0; + int status = 0; + BIO_RDP_TLS* tls = (BIO_RDP_TLS*)BIO_get_data(bio); + + if (!buf || !tls) + return 0; + + BIO_clear_flags(bio, BIO_FLAGS_WRITE | BIO_FLAGS_READ | BIO_FLAGS_IO_SPECIAL); + EnterCriticalSection(&tls->lock); + status = SSL_write(tls->ssl, buf, size); + error = SSL_get_error(tls->ssl, status); + LeaveCriticalSection(&tls->lock); + + if (status <= 0) + { + switch (error) + { + case SSL_ERROR_NONE: + BIO_clear_flags(bio, BIO_FLAGS_SHOULD_RETRY); + break; + + case SSL_ERROR_WANT_WRITE: + BIO_set_flags(bio, BIO_FLAGS_WRITE | BIO_FLAGS_SHOULD_RETRY); + break; + + case SSL_ERROR_WANT_READ: + BIO_set_flags(bio, BIO_FLAGS_READ | BIO_FLAGS_SHOULD_RETRY); + break; + + case SSL_ERROR_WANT_X509_LOOKUP: + BIO_set_flags(bio, BIO_FLAGS_IO_SPECIAL); + BIO_set_retry_reason(bio, BIO_RR_SSL_X509_LOOKUP); + break; + + case SSL_ERROR_WANT_CONNECT: + BIO_set_flags(bio, BIO_FLAGS_IO_SPECIAL); + BIO_set_retry_reason(bio, BIO_RR_CONNECT); + break; + + case SSL_ERROR_SYSCALL: + BIO_clear_flags(bio, BIO_FLAGS_SHOULD_RETRY); + break; + + case SSL_ERROR_SSL: + BIO_clear_flags(bio, BIO_FLAGS_SHOULD_RETRY); + break; + } + } + + return status; +} + +static int bio_rdp_tls_read(BIO* bio, char* buf, int size) +{ + int error = 0; + int status = 0; + BIO_RDP_TLS* tls = (BIO_RDP_TLS*)BIO_get_data(bio); + + if (!buf || !tls) + return 0; + + BIO_clear_flags(bio, BIO_FLAGS_WRITE | BIO_FLAGS_READ | BIO_FLAGS_IO_SPECIAL); + EnterCriticalSection(&tls->lock); + status = SSL_read(tls->ssl, buf, size); + error = SSL_get_error(tls->ssl, status); + LeaveCriticalSection(&tls->lock); + + if (status <= 0) + { + switch (error) + { + case SSL_ERROR_NONE: + BIO_clear_flags(bio, BIO_FLAGS_SHOULD_RETRY); + break; + + case SSL_ERROR_WANT_READ: + BIO_set_flags(bio, BIO_FLAGS_READ | BIO_FLAGS_SHOULD_RETRY); + break; + + case SSL_ERROR_WANT_WRITE: + BIO_set_flags(bio, BIO_FLAGS_WRITE | BIO_FLAGS_SHOULD_RETRY); + break; + + case SSL_ERROR_WANT_X509_LOOKUP: + BIO_set_flags(bio, BIO_FLAGS_IO_SPECIAL); + BIO_set_retry_reason(bio, BIO_RR_SSL_X509_LOOKUP); + break; + + case SSL_ERROR_WANT_ACCEPT: + BIO_set_flags(bio, BIO_FLAGS_IO_SPECIAL); + BIO_set_retry_reason(bio, BIO_RR_ACCEPT); + break; + + case SSL_ERROR_WANT_CONNECT: + BIO_set_flags(bio, BIO_FLAGS_IO_SPECIAL); + BIO_set_retry_reason(bio, BIO_RR_CONNECT); + break; + + case SSL_ERROR_SSL: + BIO_clear_flags(bio, BIO_FLAGS_SHOULD_RETRY); + break; + + case SSL_ERROR_ZERO_RETURN: + BIO_clear_flags(bio, BIO_FLAGS_SHOULD_RETRY); + break; + + case SSL_ERROR_SYSCALL: + BIO_clear_flags(bio, BIO_FLAGS_SHOULD_RETRY); + break; + } + } + +#ifdef FREERDP_HAVE_VALGRIND_MEMCHECK_H + + if (status > 0) + { + VALGRIND_MAKE_MEM_DEFINED(buf, status); + } + +#endif + return status; +} + +static int bio_rdp_tls_puts(BIO* bio, const char* str) +{ + size_t size = 0; + int status = 0; + + if (!str) + return 0; + + size = strlen(str); + ERR_clear_error(); + status = BIO_write(bio, str, size); + return status; +} + +static int bio_rdp_tls_gets(BIO* bio, char* str, int size) +{ + return 1; +} + +static long bio_rdp_tls_ctrl(BIO* bio, int cmd, long num, void* ptr) +{ + BIO* ssl_rbio = NULL; + BIO* ssl_wbio = NULL; + BIO* next_bio = NULL; + int status = -1; + BIO_RDP_TLS* tls = (BIO_RDP_TLS*)BIO_get_data(bio); + + if (!tls) + return 0; + + if (!tls->ssl && (cmd != BIO_C_SET_SSL)) + return 0; + + next_bio = BIO_next(bio); + ssl_rbio = tls->ssl ? SSL_get_rbio(tls->ssl) : NULL; + ssl_wbio = tls->ssl ? SSL_get_wbio(tls->ssl) : NULL; + + switch (cmd) + { + case BIO_CTRL_RESET: + SSL_shutdown(tls->ssl); + + if (SSL_in_connect_init(tls->ssl)) + SSL_set_connect_state(tls->ssl); + else if (SSL_in_accept_init(tls->ssl)) + SSL_set_accept_state(tls->ssl); + + SSL_clear(tls->ssl); + + if (next_bio) + status = BIO_ctrl(next_bio, cmd, num, ptr); + else if (ssl_rbio) + status = BIO_ctrl(ssl_rbio, cmd, num, ptr); + else + status = 1; + + break; + + case BIO_C_GET_FD: + status = BIO_ctrl(ssl_rbio, cmd, num, ptr); + break; + + case BIO_CTRL_INFO: + status = 0; + break; + + case BIO_CTRL_SET_CALLBACK: + status = 0; + break; + + case BIO_CTRL_GET_CALLBACK: + *((ULONG_PTR*)ptr) = (ULONG_PTR)SSL_get_info_callback(tls->ssl); + status = 1; + break; + + case BIO_C_SSL_MODE: + if (num) + SSL_set_connect_state(tls->ssl); + else + SSL_set_accept_state(tls->ssl); + + status = 1; + break; + + case BIO_CTRL_GET_CLOSE: + status = BIO_get_shutdown(bio); + break; + + case BIO_CTRL_SET_CLOSE: + BIO_set_shutdown(bio, (int)num); + status = 1; + break; + + case BIO_CTRL_WPENDING: + status = BIO_ctrl(ssl_wbio, cmd, num, ptr); + break; + + case BIO_CTRL_PENDING: + status = SSL_pending(tls->ssl); + + if (status == 0) + status = BIO_pending(ssl_rbio); + + break; + + case BIO_CTRL_FLUSH: + BIO_clear_retry_flags(bio); + status = BIO_ctrl(ssl_wbio, cmd, num, ptr); + if (status != 1) + WLog_DBG(TAG, "BIO_ctrl returned %d", status); + BIO_copy_next_retry(bio); + status = 1; + break; + + case BIO_CTRL_PUSH: + if (next_bio && (next_bio != ssl_rbio)) + { +#if OPENSSL_VERSION_NUMBER < 0x10100000L || \ + (defined(LIBRESSL_VERSION_NUMBER) && LIBRESSL_VERSION_NUMBER < 0x2070000fL) + SSL_set_bio(tls->ssl, next_bio, next_bio); + CRYPTO_add(&(bio->next_bio->references), 1, CRYPTO_LOCK_BIO); +#else + /* + * We are going to pass ownership of next to the SSL object...but + * we don't own a reference to pass yet - so up ref + */ + BIO_up_ref(next_bio); + SSL_set_bio(tls->ssl, next_bio, next_bio); +#endif + } + + status = 1; + break; + + case BIO_CTRL_POP: + + /* Only detach if we are the BIO explicitly being popped */ + if (bio == ptr) + { + if (ssl_rbio != ssl_wbio) + BIO_free_all(ssl_wbio); + +#if OPENSSL_VERSION_NUMBER < 0x10100000L || \ + (defined(LIBRESSL_VERSION_NUMBER) && LIBRESSL_VERSION_NUMBER < 0x2070000fL) + + if (next_bio) + CRYPTO_add(&(bio->next_bio->references), -1, CRYPTO_LOCK_BIO); + + tls->ssl->wbio = tls->ssl->rbio = NULL; +#else + /* OpenSSL 1.1: This will also clear the reference we obtained during push */ + SSL_set_bio(tls->ssl, NULL, NULL); +#endif + } + + status = 1; + break; + + case BIO_C_GET_SSL: + if (ptr) + { + *((SSL**)ptr) = tls->ssl; + status = 1; + } + + break; + + case BIO_C_SET_SSL: + BIO_set_shutdown(bio, (int)num); + + if (ptr) + { + tls->ssl = (SSL*)ptr; + ssl_rbio = SSL_get_rbio(tls->ssl); + } + + if (ssl_rbio) + { + if (next_bio) + BIO_push(ssl_rbio, next_bio); + + BIO_set_next(bio, ssl_rbio); +#if OPENSSL_VERSION_NUMBER < 0x10100000L || \ + (defined(LIBRESSL_VERSION_NUMBER) && LIBRESSL_VERSION_NUMBER < 0x2070000fL) + CRYPTO_add(&(ssl_rbio->references), 1, CRYPTO_LOCK_BIO); +#else + BIO_up_ref(ssl_rbio); +#endif + } + + BIO_set_init(bio, 1); + status = 1; + break; + + case BIO_C_DO_STATE_MACHINE: + BIO_clear_flags(bio, BIO_FLAGS_READ | BIO_FLAGS_WRITE | BIO_FLAGS_IO_SPECIAL); + BIO_set_retry_reason(bio, 0); + status = SSL_do_handshake(tls->ssl); + + if (status <= 0) + { + switch (SSL_get_error(tls->ssl, status)) + { + case SSL_ERROR_WANT_READ: + BIO_set_flags(bio, BIO_FLAGS_READ | BIO_FLAGS_SHOULD_RETRY); + break; + + case SSL_ERROR_WANT_WRITE: + BIO_set_flags(bio, BIO_FLAGS_WRITE | BIO_FLAGS_SHOULD_RETRY); + break; + + case SSL_ERROR_WANT_CONNECT: + BIO_set_flags(bio, BIO_FLAGS_IO_SPECIAL | BIO_FLAGS_SHOULD_RETRY); + BIO_set_retry_reason(bio, BIO_get_retry_reason(next_bio)); + break; + + default: + BIO_clear_flags(bio, BIO_FLAGS_SHOULD_RETRY); + break; + } + } + + break; + + default: + status = BIO_ctrl(ssl_rbio, cmd, num, ptr); + break; + } + + return status; +} + +static int bio_rdp_tls_new(BIO* bio) +{ + BIO_RDP_TLS* tls = NULL; + BIO_set_flags(bio, BIO_FLAGS_SHOULD_RETRY); + + if (!(tls = calloc(1, sizeof(BIO_RDP_TLS)))) + return 0; + + InitializeCriticalSectionAndSpinCount(&tls->lock, 4000); + BIO_set_data(bio, (void*)tls); + return 1; +} + +static int bio_rdp_tls_free(BIO* bio) +{ + BIO_RDP_TLS* tls = NULL; + + if (!bio) + return 0; + + tls = (BIO_RDP_TLS*)BIO_get_data(bio); + + if (!tls) + return 0; + + BIO_set_data(bio, NULL); + if (BIO_get_shutdown(bio)) + { + if (BIO_get_init(bio) && tls->ssl) + { + SSL_shutdown(tls->ssl); + SSL_free(tls->ssl); + } + + BIO_set_init(bio, 0); + BIO_set_flags(bio, 0); + } + + DeleteCriticalSection(&tls->lock); + free(tls); + + return 1; +} + +static long bio_rdp_tls_callback_ctrl(BIO* bio, int cmd, bio_info_cb* fp) +{ + long status = 0; + BIO_RDP_TLS* tls = NULL; + + if (!bio) + return 0; + + tls = (BIO_RDP_TLS*)BIO_get_data(bio); + + if (!tls) + return 0; + + switch (cmd) + { + case BIO_CTRL_SET_CALLBACK: + { + typedef void (*fkt_t)(const SSL*, int, int); + /* Documented since https://www.openssl.org/docs/man1.1.1/man3/BIO_set_callback.html + * the argument is not really of type bio_info_cb* and must be cast + * to the required type */ + fkt_t fkt = (fkt_t)(void*)fp; + SSL_set_info_callback(tls->ssl, fkt); + status = 1; + } + break; + + default: + status = BIO_callback_ctrl(SSL_get_rbio(tls->ssl), cmd, fp); + break; + } + + return status; +} + +#define BIO_TYPE_RDP_TLS 68 + +static BIO_METHOD* BIO_s_rdp_tls(void) +{ + static BIO_METHOD* bio_methods = NULL; + + if (bio_methods == NULL) + { + if (!(bio_methods = BIO_meth_new(BIO_TYPE_RDP_TLS, "RdpTls"))) + return NULL; + + BIO_meth_set_write(bio_methods, bio_rdp_tls_write); + BIO_meth_set_read(bio_methods, bio_rdp_tls_read); + BIO_meth_set_puts(bio_methods, bio_rdp_tls_puts); + BIO_meth_set_gets(bio_methods, bio_rdp_tls_gets); + BIO_meth_set_ctrl(bio_methods, bio_rdp_tls_ctrl); + BIO_meth_set_create(bio_methods, bio_rdp_tls_new); + BIO_meth_set_destroy(bio_methods, bio_rdp_tls_free); + BIO_meth_set_callback_ctrl(bio_methods, bio_rdp_tls_callback_ctrl); + } + + return bio_methods; +} + +static BIO* BIO_new_rdp_tls(SSL_CTX* ctx, int client) +{ + BIO* bio = NULL; + SSL* ssl = NULL; + bio = BIO_new(BIO_s_rdp_tls()); + + if (!bio) + return NULL; + + ssl = SSL_new(ctx); + + if (!ssl) + { + BIO_free_all(bio); + return NULL; + } + + if (client) + SSL_set_connect_state(ssl); + else + SSL_set_accept_state(ssl); + + BIO_set_ssl(bio, ssl, BIO_CLOSE); + return bio; +} + +static rdpCertificate* tls_get_certificate(rdpTls* tls, BOOL peer) +{ + X509* remote_cert = NULL; + + if (peer) + remote_cert = SSL_get_peer_certificate(tls->ssl); + else + remote_cert = X509_dup(SSL_get_certificate(tls->ssl)); + + if (!remote_cert) + { + WLog_ERR(TAG, "failed to get the server TLS certificate"); + return NULL; + } + + /* Get the peer's chain. If it does not exist, we're setting NULL (clean data either way) */ + STACK_OF(X509)* chain = SSL_get_peer_cert_chain(tls->ssl); + rdpCertificate* cert = freerdp_certificate_new_from_x509(remote_cert, chain); + X509_free(remote_cert); + + return cert; +} + +static const char* tls_get_server_name(rdpTls* tls) +{ + return tls->serverName ? tls->serverName : tls->hostname; +} + +#define TLS_SERVER_END_POINT "tls-server-end-point:" + +static SecPkgContext_Bindings* tls_get_channel_bindings(const rdpCertificate* cert) +{ + size_t CertificateHashLength = 0; + BYTE* ChannelBindingToken = NULL; + UINT32 ChannelBindingTokenLength = 0; + SEC_CHANNEL_BINDINGS* ChannelBindings = NULL; + SecPkgContext_Bindings* ContextBindings = NULL; + const size_t PrefixLength = strnlen(TLS_SERVER_END_POINT, ARRAYSIZE(TLS_SERVER_END_POINT)); + + WINPR_ASSERT(cert); + + /* See https://www.rfc-editor.org/rfc/rfc5929 for details about hashes */ + WINPR_MD_TYPE alg = freerdp_certificate_get_signature_alg(cert); + const char* hash = NULL; + switch (alg) + { + + case WINPR_MD_MD5: + case WINPR_MD_SHA1: + hash = winpr_md_type_to_string(WINPR_MD_SHA256); + break; + default: + hash = winpr_md_type_to_string(alg); + break; + } + if (!hash) + return NULL; + + char* CertificateHash = freerdp_certificate_get_hash(cert, hash, &CertificateHashLength); + if (!CertificateHash) + return NULL; + + ChannelBindingTokenLength = PrefixLength + CertificateHashLength; + ContextBindings = (SecPkgContext_Bindings*)calloc(1, sizeof(SecPkgContext_Bindings)); + + if (!ContextBindings) + goto out_free; + + ContextBindings->BindingsLength = sizeof(SEC_CHANNEL_BINDINGS) + ChannelBindingTokenLength; + ChannelBindings = (SEC_CHANNEL_BINDINGS*)calloc(1, ContextBindings->BindingsLength); + + if (!ChannelBindings) + goto out_free; + + ContextBindings->Bindings = ChannelBindings; + ChannelBindings->cbApplicationDataLength = ChannelBindingTokenLength; + ChannelBindings->dwApplicationDataOffset = sizeof(SEC_CHANNEL_BINDINGS); + ChannelBindingToken = &((BYTE*)ChannelBindings)[ChannelBindings->dwApplicationDataOffset]; + memcpy(ChannelBindingToken, TLS_SERVER_END_POINT, PrefixLength); + memcpy(ChannelBindingToken + PrefixLength, CertificateHash, CertificateHashLength); + free(CertificateHash); + return ContextBindings; +out_free: + free(CertificateHash); + free(ContextBindings); + return NULL; +} + +static INIT_ONCE secrets_file_idx_once = INIT_ONCE_STATIC_INIT; +static int secrets_file_idx = -1; + +static BOOL CALLBACK secrets_file_init_cb(PINIT_ONCE once, PVOID param, PVOID* context) +{ + secrets_file_idx = SSL_get_ex_new_index(0, NULL, NULL, NULL, NULL); + + return (secrets_file_idx != -1); +} + +static void SSLCTX_keylog_cb(const SSL* ssl, const char* line) +{ + char* dfile = NULL; + + if (secrets_file_idx == -1) + return; + + dfile = SSL_get_ex_data(ssl, secrets_file_idx); + if (dfile) + { + FILE* f = winpr_fopen(dfile, "a+"); + if (f) + { + fwrite(line, strlen(line), 1, f); + fwrite("\n", 1, 1, f); + fclose(f); + } + } +} + +static void tls_reset(rdpTls* tls) +{ + WINPR_ASSERT(tls); + + if (tls->ctx) + { + SSL_CTX_free(tls->ctx); + tls->ctx = NULL; + } + + /* tls->underlying is a stacked BIO under tls->bio. + * BIO_free_all will free recursivly. */ + if (tls->bio) + BIO_free_all(tls->bio); + else if (tls->underlying) + BIO_free_all(tls->underlying); + tls->bio = NULL; + tls->underlying = NULL; + + free_tls_public_key(tls); + free_tls_bindings(tls); +} + +#if OPENSSL_VERSION_NUMBER >= 0x010000000L +static BOOL tls_prepare(rdpTls* tls, BIO* underlying, const SSL_METHOD* method, int options, + BOOL clientMode) +#else +static BOOL tls_prepare(rdpTls* tls, BIO* underlying, SSL_METHOD* method, int options, + BOOL clientMode) +#endif +{ + WINPR_ASSERT(tls); + + rdpSettings* settings = tls->settings; + WINPR_ASSERT(settings); + + tls_reset(tls); + tls->ctx = SSL_CTX_new(method); + + tls->underlying = underlying; + + if (!tls->ctx) + { + WLog_ERR(TAG, "SSL_CTX_new failed"); + return FALSE; + } + + SSL_CTX_set_mode(tls->ctx, SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER | SSL_MODE_ENABLE_PARTIAL_WRITE); + SSL_CTX_set_options(tls->ctx, options); + SSL_CTX_set_read_ahead(tls->ctx, 1); +#if OPENSSL_VERSION_NUMBER >= 0x10100000L || defined(LIBRESSL_VERSION_NUMBER) + UINT16 version = freerdp_settings_get_uint16(settings, FreeRDP_TLSMinVersion); + if (!SSL_CTX_set_min_proto_version(tls->ctx, version)) + { + WLog_ERR(TAG, "SSL_CTX_set_min_proto_version %s failed", version); + return FALSE; + } + version = freerdp_settings_get_uint16(settings, FreeRDP_TLSMaxVersion); + if (!SSL_CTX_set_max_proto_version(tls->ctx, version)) + { + WLog_ERR(TAG, "SSL_CTX_set_max_proto_version %s failed", version); + return FALSE; + } +#endif +#if OPENSSL_VERSION_NUMBER >= 0x10100000L && !defined(LIBRESSL_VERSION_NUMBER) + SSL_CTX_set_security_level(tls->ctx, settings->TlsSecLevel); +#endif + + if (settings->AllowedTlsCiphers) + { + if (!SSL_CTX_set_cipher_list(tls->ctx, settings->AllowedTlsCiphers)) + { + WLog_ERR(TAG, "SSL_CTX_set_cipher_list %s failed", settings->AllowedTlsCiphers); + return FALSE; + } + } + + tls->bio = BIO_new_rdp_tls(tls->ctx, clientMode); + + if (BIO_get_ssl(tls->bio, &tls->ssl) < 0) + { + WLog_ERR(TAG, "unable to retrieve the SSL of the connection"); + return FALSE; + } + + if (settings->TlsSecretsFile) + { +#if OPENSSL_VERSION_NUMBER >= 0x10101000L + InitOnceExecuteOnce(&secrets_file_idx_once, secrets_file_init_cb, NULL, NULL); + + if (secrets_file_idx != -1) + { + SSL_set_ex_data(tls->ssl, secrets_file_idx, settings->TlsSecretsFile); + SSL_CTX_set_keylog_callback(tls->ctx, SSLCTX_keylog_cb); + } +#else + WLog_WARN(TAG, "Key-Logging not available - requires OpenSSL 1.1.1 or higher"); +#endif + } + + BIO_push(tls->bio, underlying); + return TRUE; +} + +static void adjustSslOptions(int* options) +{ + WINPR_ASSERT(options); +#if OPENSSL_VERSION_NUMBER < 0x10100000L || defined(LIBRESSL_VERSION_NUMBER) + *options |= SSL_OP_NO_SSLv2; + *options |= SSL_OP_NO_SSLv3; +#endif +} + +const SSL_METHOD* freerdp_tls_get_ssl_method(BOOL isDtls, BOOL isClient) +{ + if (isClient) + { + if (isDtls) + return DTLS_client_method(); +#if OPENSSL_VERSION_NUMBER < 0x10100000L || defined(LIBRESSL_VERSION_NUMBER) + return SSLv23_client_method(); +#else + return TLS_client_method(); +#endif + } + + if (isDtls) + return DTLS_server_method(); + +#if OPENSSL_VERSION_NUMBER < 0x10100000L || defined(LIBRESSL_VERSION_NUMBER) + return SSLv23_server_method(); +#else + return TLS_server_method(); +#endif +} + +TlsHandshakeResult freerdp_tls_connect_ex(rdpTls* tls, BIO* underlying, const SSL_METHOD* methods) +{ + WINPR_ASSERT(tls); + + int options = 0; + /** + * SSL_OP_NO_COMPRESSION: + * + * The Microsoft RDP server does not advertise support + * for TLS compression, but alternative servers may support it. + * This was observed between early versions of the FreeRDP server + * and the FreeRDP client, and caused major performance issues, + * which is why we're disabling it. + */ +#ifdef SSL_OP_NO_COMPRESSION + options |= SSL_OP_NO_COMPRESSION; +#endif + /** + * SSL_OP_TLS_BLOCK_PADDING_BUG: + * + * The Microsoft RDP server does *not* support TLS padding. + * It absolutely needs to be disabled otherwise it won't work. + */ + options |= SSL_OP_TLS_BLOCK_PADDING_BUG; + /** + * SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS: + * + * Just like TLS padding, the Microsoft RDP server does not + * support empty fragments. This needs to be disabled. + */ + options |= SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS; + + tls->isClientMode = TRUE; + adjustSslOptions(&options); + + if (!tls_prepare(tls, underlying, methods, options, TRUE)) + return 0; + +#if !defined(OPENSSL_NO_TLSEXT) && !defined(LIBRESSL_VERSION_NUMBER) + SSL_set_tlsext_host_name(tls->ssl, tls_get_server_name(tls)); +#endif + + return freerdp_tls_handshake(tls); +} + +TlsHandshakeResult freerdp_tls_handshake(rdpTls* tls) +{ + TlsHandshakeResult ret = TLS_HANDSHAKE_ERROR; + + WINPR_ASSERT(tls); + int status = BIO_do_handshake(tls->bio); + if (status != 1) + { + if (!BIO_should_retry(tls->bio)) + return TLS_HANDSHAKE_ERROR; + + return TLS_HANDSHAKE_CONTINUE; + } + + int verify_status = 0; + rdpCertificate* cert = tls_get_certificate(tls, tls->isClientMode); + + if (!cert) + { + WLog_ERR(TAG, "tls_get_certificate failed to return the server certificate."); + return TLS_HANDSHAKE_ERROR; + } + + do + { + free_tls_bindings(tls); + tls->Bindings = tls_get_channel_bindings(cert); + if (!tls->Bindings) + { + WLog_ERR(TAG, "unable to retrieve bindings"); + break; + } + + free_tls_public_key(tls); + if (!freerdp_certificate_get_public_key(cert, &tls->PublicKey, &tls->PublicKeyLength)) + { + WLog_ERR(TAG, + "freerdp_certificate_get_public_key failed to return the server public key."); + break; + } + + /* server-side NLA needs public keys (keys from us, the server) but no certificate verify */ + ret = TLS_HANDSHAKE_SUCCESS; + + if (tls->isClientMode) + { + verify_status = tls_verify_certificate(tls, cert, tls_get_server_name(tls), tls->port); + + if (verify_status < 1) + { + WLog_ERR(TAG, "certificate not trusted, aborting."); + freerdp_tls_send_alert(tls); + ret = TLS_HANDSHAKE_VERIFY_ERROR; + } + } + } while (0); + + freerdp_certificate_free(cert); + return ret; +} + +static int pollAndHandshake(rdpTls* tls) +{ + WINPR_ASSERT(tls); + + do + { + HANDLE event = NULL; + DWORD status = 0; + if (BIO_get_event(tls->bio, &event) < 0) + { + WLog_ERR(TAG, "unable to retrieve BIO associated event"); + return -1; + } + + if (!event) + { + WLog_ERR(TAG, "unable to retrieve BIO event"); + return -1; + } + + status = WaitForSingleObjectEx(event, 50, TRUE); + switch (status) + { + case WAIT_OBJECT_0: + break; + case WAIT_TIMEOUT: + continue; + default: + WLog_ERR(TAG, "error during WaitForSingleObject(): 0x%08" PRIX32 "", status); + return -1; + } + + TlsHandshakeResult result = freerdp_tls_handshake(tls); + switch (result) + { + case TLS_HANDSHAKE_CONTINUE: + break; + case TLS_HANDSHAKE_SUCCESS: + return 1; + case TLS_HANDSHAKE_ERROR: + case TLS_HANDSHAKE_VERIFY_ERROR: + default: + return -1; + } + } while (TRUE); +} + +int freerdp_tls_connect(rdpTls* tls, BIO* underlying) +{ + const SSL_METHOD* method = freerdp_tls_get_ssl_method(FALSE, TRUE); + + WINPR_ASSERT(tls); + TlsHandshakeResult result = freerdp_tls_connect_ex(tls, underlying, method); + switch (result) + { + case TLS_HANDSHAKE_SUCCESS: + return 1; + case TLS_HANDSHAKE_CONTINUE: + break; + case TLS_HANDSHAKE_ERROR: + case TLS_HANDSHAKE_VERIFY_ERROR: + return -1; + } + + return pollAndHandshake(tls); +} + +#if defined(MICROSOFT_IOS_SNI_BUG) && !defined(OPENSSL_NO_TLSEXT) && \ + !defined(LIBRESSL_VERSION_NUMBER) +static void tls_openssl_tlsext_debug_callback(SSL* s, int client_server, int type, + unsigned char* data, int len, void* arg) +{ + if (type == TLSEXT_TYPE_server_name) + { + WLog_DBG(TAG, "Client uses SNI (extension disabled)"); + s->servername_done = 2; + } +} +#endif + +BOOL freerdp_tls_accept(rdpTls* tls, BIO* underlying, rdpSettings* settings) +{ + WINPR_ASSERT(tls); + TlsHandshakeResult res = + freerdp_tls_accept_ex(tls, underlying, settings, freerdp_tls_get_ssl_method(FALSE, FALSE)); + switch (res) + { + case TLS_HANDSHAKE_SUCCESS: + return TRUE; + case TLS_HANDSHAKE_CONTINUE: + break; + case TLS_HANDSHAKE_ERROR: + case TLS_HANDSHAKE_VERIFY_ERROR: + default: + return FALSE; + } + + return pollAndHandshake(tls) > 0; +} + +TlsHandshakeResult freerdp_tls_accept_ex(rdpTls* tls, BIO* underlying, rdpSettings* settings, + const SSL_METHOD* methods) +{ + WINPR_ASSERT(tls); + + long options = 0; + int status = 0; + + /** + * SSL_OP_NO_SSLv2: + * + * We only want SSLv3 and TLSv1, so disable SSLv2. + * SSLv3 is used by, eg. Microsoft RDC for Mac OS X. + */ + options |= SSL_OP_NO_SSLv2; + /** + * SSL_OP_NO_COMPRESSION: + * + * The Microsoft RDP server does not advertise support + * for TLS compression, but alternative servers may support it. + * This was observed between early versions of the FreeRDP server + * and the FreeRDP client, and caused major performance issues, + * which is why we're disabling it. + */ +#ifdef SSL_OP_NO_COMPRESSION + options |= SSL_OP_NO_COMPRESSION; +#endif + /** + * SSL_OP_TLS_BLOCK_PADDING_BUG: + * + * The Microsoft RDP server does *not* support TLS padding. + * It absolutely needs to be disabled otherwise it won't work. + */ + options |= SSL_OP_TLS_BLOCK_PADDING_BUG; + /** + * SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS: + * + * Just like TLS padding, the Microsoft RDP server does not + * support empty fragments. This needs to be disabled. + */ + options |= SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS; + + /** + * SSL_OP_NO_RENEGOTIATION + * + * Disable SSL client site renegotiation. + */ + +#if (OPENSSL_VERSION_NUMBER >= 0x10100000L) && (OPENSSL_VERSION_NUMBER < 0x30000000L) && \ + !defined(LIBRESSL_VERSION_NUMBER) + options |= SSL_OP_NO_RENEGOTIATION; +#endif + + if (!tls_prepare(tls, underlying, methods, options, FALSE)) + return TLS_HANDSHAKE_ERROR; + + const rdpPrivateKey* key = freerdp_settings_get_pointer(settings, FreeRDP_RdpServerRsaKey); + if (!key) + { + WLog_ERR(TAG, "invalid private key"); + return TLS_HANDSHAKE_ERROR; + } + + EVP_PKEY* privkey = freerdp_key_get_evp_pkey(key); + if (!privkey) + { + WLog_ERR(TAG, "invalid private key"); + return TLS_HANDSHAKE_ERROR; + } + + status = SSL_use_PrivateKey(tls->ssl, privkey); + /* The local reference to the private key will anyway go out of + * scope; so the reference count should be decremented weither + * SSL_use_PrivateKey succeeds or fails. + */ + EVP_PKEY_free(privkey); + + if (status <= 0) + { + WLog_ERR(TAG, "SSL_CTX_use_PrivateKey_file failed"); + return TLS_HANDSHAKE_ERROR; + } + + rdpCertificate* cert = + freerdp_settings_get_pointer_writable(settings, FreeRDP_RdpServerCertificate); + if (!cert) + { + WLog_ERR(TAG, "invalid certificate"); + return TLS_HANDSHAKE_ERROR; + } + + status = SSL_use_certificate(tls->ssl, freerdp_certificate_get_x509(cert)); + + if (status <= 0) + { + WLog_ERR(TAG, "SSL_use_certificate_file failed"); + return TLS_HANDSHAKE_ERROR; + } + +#if defined(MICROSOFT_IOS_SNI_BUG) && !defined(OPENSSL_NO_TLSEXT) && \ + !defined(LIBRESSL_VERSION_NUMBER) + SSL_set_tlsext_debug_callback(tls->ssl, tls_openssl_tlsext_debug_callback); +#endif + + return freerdp_tls_handshake(tls); +} + +BOOL freerdp_tls_send_alert(rdpTls* tls) +{ + WINPR_ASSERT(tls); + + if (!tls) + return FALSE; + + if (!tls->ssl) + return TRUE; + + /** + * FIXME: The following code does not work on OpenSSL > 1.1.0 because the + * SSL struct is opaqe now + */ +#if (!defined(LIBRESSL_VERSION_NUMBER) && (OPENSSL_VERSION_NUMBER < 0x10100000L)) || \ + (defined(LIBRESSL_VERSION_NUMBER) && (LIBRESSL_VERSION_NUMBER <= 0x2080300fL)) + + if (tls->alertDescription != TLS_ALERT_DESCRIPTION_CLOSE_NOTIFY) + { + /** + * OpenSSL doesn't really expose an API for sending a TLS alert manually. + * + * The following code disables the sending of the default "close notify" + * and then proceeds to force sending a custom TLS alert before shutting down. + * + * Manually sending a TLS alert is necessary in certain cases, + * like when server-side NLA results in an authentication failure. + */ + SSL_SESSION* ssl_session = SSL_get_session(tls->ssl); + SSL_CTX* ssl_ctx = SSL_get_SSL_CTX(tls->ssl); + SSL_set_quiet_shutdown(tls->ssl, 1); + + if ((tls->alertLevel == TLS_ALERT_LEVEL_FATAL) && (ssl_session)) + SSL_CTX_remove_session(ssl_ctx, ssl_session); + + tls->ssl->s3->alert_dispatch = 1; + tls->ssl->s3->send_alert[0] = tls->alertLevel; + tls->ssl->s3->send_alert[1] = tls->alertDescription; + + if (tls->ssl->s3->wbuf.left == 0) + tls->ssl->method->ssl_dispatch_alert(tls->ssl); + } + +#endif + return TRUE; +} + +int freerdp_tls_write_all(rdpTls* tls, const BYTE* data, int length) +{ + WINPR_ASSERT(tls); + int status = 0; + int offset = 0; + BIO* bio = tls->bio; + + while (offset < length) + { + ERR_clear_error(); + status = BIO_write(bio, &data[offset], length - offset); + + if (status > 0) + { + offset += status; + } + else + { + if (!BIO_should_retry(bio)) + return -1; + + if (BIO_write_blocked(bio)) + status = BIO_wait_write(bio, 100); + else if (BIO_read_blocked(bio)) + return -2; /* Abort write, there is data that must be read */ + else + USleep(100); + + if (status < 0) + return -1; + } + } + + return length; +} + +int freerdp_tls_set_alert_code(rdpTls* tls, int level, int description) +{ + WINPR_ASSERT(tls); + tls->alertLevel = level; + tls->alertDescription = description; + return 0; +} + +static BOOL tls_match_hostname(const char* pattern, const size_t pattern_length, + const char* hostname) +{ + if (strlen(hostname) == pattern_length) + { + if (_strnicmp(hostname, pattern, pattern_length) == 0) + return TRUE; + } + + if ((pattern_length > 2) && (pattern[0] == '*') && (pattern[1] == '.') && + ((strlen(hostname)) >= pattern_length)) + { + const char* check_hostname = &hostname[strlen(hostname) - pattern_length + 1]; + + if (_strnicmp(check_hostname, &pattern[1], pattern_length - 1) == 0) + { + return TRUE; + } + } + + return FALSE; +} + +static BOOL is_redirected(rdpTls* tls) +{ + rdpSettings* settings = tls->settings; + + if (LB_NOREDIRECT & settings->RedirectionFlags) + return FALSE; + + return settings->RedirectionFlags != 0; +} + +static BOOL is_accepted(rdpTls* tls, const BYTE* pem, size_t length) +{ + rdpSettings* settings = tls->settings; + char* AccpetedKey = NULL; + UINT32 AcceptedKeyLength = 0; + + if (tls->isGatewayTransport) + { + AccpetedKey = settings->GatewayAcceptedCert; + AcceptedKeyLength = settings->GatewayAcceptedCertLength; + } + else if (is_redirected(tls)) + { + AccpetedKey = settings->RedirectionAcceptedCert; + AcceptedKeyLength = settings->RedirectionAcceptedCertLength; + } + else + { + AccpetedKey = settings->AcceptedCert; + AcceptedKeyLength = settings->AcceptedCertLength; + } + + if (AcceptedKeyLength > 0) + { + if (AcceptedKeyLength == length) + { + if (memcmp(AccpetedKey, pem, AcceptedKeyLength) == 0) + return TRUE; + } + } + + if (tls->isGatewayTransport) + { + free(settings->GatewayAcceptedCert); + settings->GatewayAcceptedCert = NULL; + settings->GatewayAcceptedCertLength = 0; + } + else if (is_redirected(tls)) + { + free(settings->RedirectionAcceptedCert); + settings->RedirectionAcceptedCert = NULL; + settings->RedirectionAcceptedCertLength = 0; + } + else + { + free(settings->AcceptedCert); + settings->AcceptedCert = NULL; + settings->AcceptedCertLength = 0; + } + + return FALSE; +} + +static BOOL compare_fingerprint(const char* fp, const char* hash, const rdpCertificate* cert, + BOOL separator) +{ + BOOL equal = 0; + char* strhash = NULL; + + WINPR_ASSERT(fp); + WINPR_ASSERT(hash); + WINPR_ASSERT(cert); + + strhash = freerdp_certificate_get_fingerprint_by_hash_ex(cert, hash, separator); + if (!strhash) + return FALSE; + + equal = (_stricmp(strhash, fp) == 0); + free(strhash); + return equal; +} + +static BOOL compare_fingerprint_all(const char* fp, const char* hash, const rdpCertificate* cert) +{ + WINPR_ASSERT(fp); + WINPR_ASSERT(hash); + WINPR_ASSERT(cert); + if (compare_fingerprint(fp, hash, cert, FALSE)) + return TRUE; + if (compare_fingerprint(fp, hash, cert, TRUE)) + return TRUE; + return FALSE; +} + +static BOOL is_accepted_fingerprint(const rdpCertificate* cert, + const char* CertificateAcceptedFingerprints) +{ + WINPR_ASSERT(cert); + + BOOL rc = FALSE; + if (CertificateAcceptedFingerprints) + { + char* context = NULL; + char* copy = _strdup(CertificateAcceptedFingerprints); + char* cur = strtok_s(copy, ",", &context); + while (cur) + { + char* subcontext = NULL; + const char* h = strtok_s(cur, ":", &subcontext); + const char* fp = NULL; + + if (!h) + goto next; + + fp = h + strlen(h) + 1; + if (!fp) + goto next; + + if (compare_fingerprint_all(fp, h, cert)) + { + rc = TRUE; + break; + } + next: + cur = strtok_s(NULL, ",", &context); + } + free(copy); + } + + return rc; +} + +static BOOL accept_cert(rdpTls* tls, const BYTE* pem, UINT32 length) +{ + WINPR_ASSERT(tls); + FreeRDP_Settings_Keys_String id = FreeRDP_AcceptedCert; + FreeRDP_Settings_Keys_UInt32 lid = FreeRDP_AcceptedCertLength; + + rdpSettings* settings = tls->settings; + + if (tls->isGatewayTransport) + { + id = FreeRDP_GatewayAcceptedCert; + lid = FreeRDP_GatewayAcceptedCertLength; + } + else if (is_redirected(tls)) + { + id = FreeRDP_RedirectionAcceptedCert; + lid = FreeRDP_RedirectionAcceptedCertLength; + } + + if (!freerdp_settings_set_string_len(settings, id, (const char*)pem, length)) + return FALSE; + + return freerdp_settings_set_uint32(settings, lid, length); +} + +static BOOL tls_extract_pem(const rdpCertificate* cert, BYTE** PublicKey, size_t* PublicKeyLength) +{ + if (!cert || !PublicKey) + return FALSE; + *PublicKey = (BYTE*)freerdp_certificate_get_pem(cert, PublicKeyLength); + return *PublicKey != NULL; +} + +int tls_verify_certificate(rdpTls* tls, const rdpCertificate* cert, const char* hostname, + UINT16 port) +{ + int match = 0; + size_t length = 0; + BOOL certificate_status = 0; + char* common_name = NULL; + size_t common_name_length = 0; + char** dns_names = 0; + size_t dns_names_count = 0; + size_t* dns_names_lengths = NULL; + int verification_status = -1; + BOOL hostname_match = FALSE; + rdpCertificateData* certificate_data = NULL; + BYTE* pemCert = NULL; + DWORD flags = VERIFY_CERT_FLAG_NONE; + freerdp* instance = NULL; + + WINPR_ASSERT(tls); + WINPR_ASSERT(tls->settings); + + instance = (freerdp*)tls->settings->instance; + WINPR_ASSERT(instance); + + if (freerdp_shall_disconnect_context(instance->context)) + return -1; + + if (!tls_extract_pem(cert, &pemCert, &length)) + goto end; + + /* Check, if we already accepted this key. */ + if (is_accepted(tls, pemCert, length)) + { + verification_status = 1; + goto end; + } + + if (is_accepted_fingerprint(cert, tls->settings->CertificateAcceptedFingerprints)) + { + verification_status = 1; + goto end; + } + + if (tls->isGatewayTransport || is_redirected(tls)) + flags |= VERIFY_CERT_FLAG_LEGACY; + + if (tls->isGatewayTransport) + flags |= VERIFY_CERT_FLAG_GATEWAY; + + if (is_redirected(tls)) + flags |= VERIFY_CERT_FLAG_REDIRECT; + + /* Certificate management is done by the application */ + if (tls->settings->ExternalCertificateManagement) + { + if (instance->VerifyX509Certificate) + verification_status = + instance->VerifyX509Certificate(instance, pemCert, length, hostname, port, flags); + else + WLog_ERR(TAG, "No VerifyX509Certificate callback registered!"); + + if (verification_status > 0) + accept_cert(tls, pemCert, length); + else if (verification_status < 0) + { + WLog_ERR(TAG, "VerifyX509Certificate failed: (length = %" PRIuz ") status: [%d] %s", + length, verification_status, pemCert); + goto end; + } + } + /* ignore certificate verification if user explicitly required it (discouraged) */ + else if (tls->settings->IgnoreCertificate) + verification_status = 1; /* success! */ + else if (!tls->isGatewayTransport && (tls->settings->AuthenticationLevel == 0)) + verification_status = 1; /* success! */ + else + { + /* if user explicitly specified a certificate name, use it instead of the hostname */ + if (!tls->isGatewayTransport && tls->settings->CertificateName) + hostname = tls->settings->CertificateName; + + /* attempt verification using OpenSSL and the ~/.freerdp/certs certificate store */ + certificate_status = freerdp_certificate_verify( + cert, freerdp_certificate_store_get_certs_path(tls->certificate_store)); + /* verify certificate name match */ + certificate_data = freerdp_certificate_data_new(hostname, port, cert); + if (!certificate_data) + goto end; + /* extra common name and alternative names */ + common_name = freerdp_certificate_get_common_name(cert, &common_name_length); + dns_names = freerdp_certificate_get_dns_names(cert, &dns_names_count, &dns_names_lengths); + + /* compare against common name */ + + if (common_name) + { + if (tls_match_hostname(common_name, common_name_length, hostname)) + hostname_match = TRUE; + } + + /* compare against alternative names */ + + if (dns_names) + { + for (size_t index = 0; index < dns_names_count; index++) + { + if (tls_match_hostname(dns_names[index], dns_names_lengths[index], hostname)) + { + hostname_match = TRUE; + break; + } + } + } + + /* if the certificate is valid and the certificate name matches, verification succeeds + */ + if (certificate_status && hostname_match) + verification_status = 1; /* success! */ + + if (!hostname_match) + flags |= VERIFY_CERT_FLAG_MISMATCH; + + /* verification could not succeed with OpenSSL, use known_hosts file and prompt user for + * manual verification */ + if (!certificate_status || !hostname_match) + { + DWORD accept_certificate = 0; + size_t pem_length = 0; + char* issuer = freerdp_certificate_get_issuer(cert); + char* subject = freerdp_certificate_get_subject(cert); + char* pem = freerdp_certificate_get_pem(cert, &pem_length); + + if (!pem) + goto end; + + /* search for matching entry in known_hosts file */ + match = + freerdp_certificate_store_contains_data(tls->certificate_store, certificate_data); + + if (match == 1) + { + /* no entry was found in known_hosts file, prompt user for manual verification + */ + if (!hostname_match) + tls_print_certificate_name_mismatch_error(hostname, port, common_name, + dns_names, dns_names_count); + + { + char* efp = freerdp_certificate_get_fingerprint(cert); + tls_print_new_certificate_warn(tls->certificate_store, hostname, port, efp); + free(efp); + } + + /* Automatically accept certificate on first use */ + if (tls->settings->AutoAcceptCertificate) + { + WLog_INFO(TAG, "No certificate stored, automatically accepting."); + accept_certificate = 1; + } + else if (tls->settings->AutoDenyCertificate) + { + WLog_INFO(TAG, "No certificate stored, automatically denying."); + accept_certificate = 0; + } + else if (instance->VerifyX509Certificate) + { + int rc = instance->VerifyX509Certificate(instance, pemCert, pem_length, + hostname, port, flags); + + if (rc == 1) + accept_certificate = 1; + else if (rc > 1) + accept_certificate = 2; + else + accept_certificate = 0; + } + else if (instance->VerifyCertificateEx) + { + const BOOL use_pem = freerdp_settings_get_bool( + tls->settings, FreeRDP_CertificateCallbackPreferPEM); + char* fp = NULL; + DWORD cflags = flags; + if (use_pem) + { + cflags |= VERIFY_CERT_FLAG_FP_IS_PEM; + fp = pem; + } + else + fp = freerdp_certificate_get_fingerprint(cert); + accept_certificate = instance->VerifyCertificateEx( + instance, hostname, port, common_name, subject, issuer, fp, cflags); + if (!use_pem) + free(fp); + } +#if defined(WITH_FREERDP_DEPRECATED) + else if (instance->VerifyCertificate) + { + char* fp = freerdp_certificate_get_fingerprint(cert); + + WLog_WARN(TAG, "The VerifyCertificate callback is deprecated, migrate your " + "application to VerifyCertificateEx"); + accept_certificate = instance->VerifyCertificate(instance, common_name, subject, + issuer, fp, !hostname_match); + free(fp); + } +#endif + } + else if (match == -1) + { + rdpCertificateData* stored_data = + freerdp_certificate_store_load_data(tls->certificate_store, hostname, port); + /* entry was found in known_hosts file, but fingerprint does not match. ask user + * to use it */ + { + char* efp = freerdp_certificate_get_fingerprint(cert); + tls_print_certificate_error(tls->certificate_store, stored_data, hostname, port, + efp); + free(efp); + } + + if (!stored_data) + WLog_WARN(TAG, "Failed to get certificate entry for %s:%" PRIu16 "", hostname, + port); + + if (tls->settings->AutoDenyCertificate) + { + WLog_INFO(TAG, "No certificate stored, automatically denying."); + accept_certificate = 0; + } + else if (instance->VerifyX509Certificate) + { + const int rc = + instance->VerifyX509Certificate(instance, pemCert, pem_length, hostname, + port, flags | VERIFY_CERT_FLAG_CHANGED); + + if (rc == 1) + accept_certificate = 1; + else if (rc > 1) + accept_certificate = 2; + else + accept_certificate = 0; + } + else if (instance->VerifyChangedCertificateEx) + { + DWORD cflags = flags | VERIFY_CERT_FLAG_CHANGED; + const char* old_subject = freerdp_certificate_data_get_subject(stored_data); + const char* old_issuer = freerdp_certificate_data_get_issuer(stored_data); + const char* old_fp = freerdp_certificate_data_get_fingerprint(stored_data); + const char* old_pem = freerdp_certificate_data_get_pem(stored_data); + const BOOL fpIsAllocated = + !old_pem || !freerdp_settings_get_bool( + tls->settings, FreeRDP_CertificateCallbackPreferPEM); + char* fp = NULL; + if (!fpIsAllocated) + { + cflags |= VERIFY_CERT_FLAG_FP_IS_PEM; + fp = pem; + old_fp = old_pem; + } + else + { + fp = freerdp_certificate_get_fingerprint(cert); + } + accept_certificate = instance->VerifyChangedCertificateEx( + instance, hostname, port, common_name, subject, issuer, fp, old_subject, + old_issuer, old_fp, cflags); + if (fpIsAllocated) + free(fp); + } +#if defined(WITH_FREERDP_DEPRECATED) + else if (instance->VerifyChangedCertificate) + { + char* fp = freerdp_certificate_get_fingerprint(cert); + const char* old_subject = freerdp_certificate_data_get_subject(stored_data); + const char* old_issuer = freerdp_certificate_data_get_issuer(stored_data); + const char* old_fingerprint = + freerdp_certificate_data_get_fingerprint(stored_data); + + WLog_WARN(TAG, "The VerifyChangedCertificate callback is deprecated, migrate " + "your application to VerifyChangedCertificateEx"); + accept_certificate = instance->VerifyChangedCertificate( + instance, common_name, subject, issuer, fp, old_subject, old_issuer, + old_fingerprint); + free(fp); + } +#endif + + freerdp_certificate_data_free(stored_data); + } + else if (match == 0) + accept_certificate = 2; /* success! */ + + /* Save certificate or do a simple accept / reject */ + switch (accept_certificate) + { + case 1: + + /* user accepted certificate, add entry in known_hosts file */ + verification_status = freerdp_certificate_store_save_data( + tls->certificate_store, certificate_data) + ? 1 + : -1; + break; + + case 2: + /* user did accept temporaty, do not add to known hosts file */ + verification_status = 1; + break; + + default: + /* user did not accept, abort and do not add entry in known_hosts file */ + verification_status = -1; /* failure! */ + break; + } + + free(issuer); + free(subject); + free(pem); + } + + if (verification_status > 0) + accept_cert(tls, pemCert, length); + } + +end: + freerdp_certificate_data_free(certificate_data); + free(common_name); + freerdp_certificate_free_dns_names(dns_names_count, dns_names_lengths, dns_names); + free(pemCert); + return verification_status; +} + +void tls_print_new_certificate_warn(rdpCertificateStore* store, const char* hostname, UINT16 port, + const char* fingerprint) +{ + char* path = freerdp_certificate_store_get_cert_path(store, hostname, port); + + WLog_ERR(TAG, "The host key for %s:%" PRIu16 " has changed", hostname, port); + WLog_ERR(TAG, "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"); + WLog_ERR(TAG, "@ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @"); + WLog_ERR(TAG, "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"); + WLog_ERR(TAG, "IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!"); + WLog_ERR(TAG, "Someone could be eavesdropping on you right now (man-in-the-middle attack)!"); + WLog_ERR(TAG, "It is also possible that a host key has just been changed."); + WLog_ERR(TAG, "The fingerprint for the host key sent by the remote host is %s", fingerprint); + WLog_ERR(TAG, "Please contact your system administrator."); + WLog_ERR(TAG, "Add correct host key in %s to get rid of this message.", path); + WLog_ERR(TAG, "Host key for %s has changed and you have requested strict checking.", hostname); + WLog_ERR(TAG, "Host key verification failed."); + + free(path); +} + +void tls_print_certificate_error(rdpCertificateStore* store, rdpCertificateData* stored_data, + const char* hostname, UINT16 port, const char* fingerprint) +{ + char* path = freerdp_certificate_store_get_cert_path(store, hostname, port); + + WLog_ERR(TAG, "New host key for %s:%" PRIu16, hostname, port); + WLog_ERR(TAG, "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"); + WLog_ERR(TAG, "@ WARNING: NEW HOST IDENTIFICATION! @"); + WLog_ERR(TAG, "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"); + + WLog_ERR(TAG, "The fingerprint for the host key sent by the remote host is %s", fingerprint); + WLog_ERR(TAG, "Please contact your system administrator."); + WLog_ERR(TAG, "Add correct host key in %s to get rid of this message.", path); + + free(path); +} + +void tls_print_certificate_name_mismatch_error(const char* hostname, UINT16 port, + const char* common_name, char** alt_names, + size_t alt_names_count) +{ + WINPR_ASSERT(NULL != hostname); + WLog_ERR(TAG, "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"); + WLog_ERR(TAG, "@ WARNING: CERTIFICATE NAME MISMATCH! @"); + WLog_ERR(TAG, "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"); + WLog_ERR(TAG, "The hostname used for this connection (%s:%" PRIu16 ") ", hostname, port); + WLog_ERR(TAG, "does not match %s given in the certificate:", + alt_names_count < 1 ? "the name" : "any of the names"); + WLog_ERR(TAG, "Common Name (CN):"); + WLog_ERR(TAG, "\t%s", common_name ? common_name : "no CN found in certificate"); + + if (alt_names_count > 0) + { + WINPR_ASSERT(NULL != alt_names); + WLog_ERR(TAG, "Alternative names:"); + + for (size_t index = 0; index < alt_names_count; index++) + { + WINPR_ASSERT(alt_names[index]); + WLog_ERR(TAG, "\t %s", alt_names[index]); + } + } + + WLog_ERR(TAG, "A valid certificate for the wrong name should NOT be trusted!"); +} + +rdpTls* freerdp_tls_new(rdpSettings* settings) +{ + rdpTls* tls = NULL; + tls = (rdpTls*)calloc(1, sizeof(rdpTls)); + + if (!tls) + return NULL; + + tls->settings = settings; + + if (!settings->ServerMode) + { + tls->certificate_store = freerdp_certificate_store_new(settings); + + if (!tls->certificate_store) + goto out_free; + } + + tls->alertLevel = TLS_ALERT_LEVEL_WARNING; + tls->alertDescription = TLS_ALERT_DESCRIPTION_CLOSE_NOTIFY; + return tls; +out_free: + free(tls); + return NULL; +} + +void freerdp_tls_free(rdpTls* tls) +{ + if (!tls) + return; + + tls_reset(tls); + + if (tls->certificate_store) + { + freerdp_certificate_store_free(tls->certificate_store); + tls->certificate_store = NULL; + } + + free(tls); +} |