Index: configure.ac ================================================================== --- configure.ac +++ configure.ac @@ -1782,11 +1782,12 @@ esac AC_ARG_WITH(tls, AS_HELP_STRING([--with-tls], [ enable TLS support using the specified library - (yes, openssl, gnutls, securetransport or no)])) + (yes, openssl, gnutls, securetransport, mbedtls or + no)])) AS_IF([test x"$with_tls" = x""], [with_tls="yes"]) tls_support="no" AS_IF([test x"$with_tls" = x"securetransport" \ -o x"$with_tls" = x"yes"], [ @@ -1850,10 +1851,32 @@ dnl Disable default action-if-not-found, which exits dnl configure with an error. : ]) ]) + + AS_IF([test x"$with_tls" = x"mbedtls"], [ + AC_ARG_WITH(mbedtls-ca-path, + AS_HELP_STRING([path to CA file for Mbed TLS])) + + AS_IF([test x"$with_mbedtls_ca_path" = x""], [ + AC_MSG_ERROR([--mbedtls-ca-path needs to be specified!]) + ]) + AC_DEFINE_UNQUOTED(OF_MBEDTLS_CA_PATH, "$with_mbedtls_ca_path", + [Path to CA file for Mbed TLS]) + + AC_CHECK_LIB(mbedtls, mbedtls_net_init, [ + AC_CHECK_HEADER(mbedtls/ssl.h, [ + tls_support="Mbed TLS" + TLS_LIBS="-lmbedx509 -lmbedcrypto $TLS_LIBS" + TLS_LIBS="-lmbedtls $TLS_LIBS" + + AC_SUBST(OF_MBEDTLS_TLS_STREAM_M, + "OFMbedTLSTLSStream.m") + ]) + ], [], [-lmbedx509 -lmbedcrypto]) + ]) AS_IF([test x"$tls_support" != x"no"], [ AC_SUBST(TLS, "tls") AC_SUBST(TLS_CPPFLAGS) AC_SUBST(TLS_LIBS) Index: extra.mk.in ================================================================== --- extra.mk.in +++ extra.mk.in @@ -56,10 +56,11 @@ OF_BLOCK_TESTS_M = @OF_BLOCK_TESTS_M@ OF_EPOLL_KERNEL_EVENT_OBSERVER_M = @OF_EPOLL_KERNEL_EVENT_OBSERVER_M@ OF_GNUTLS_TLS_STREAM_M = @OF_GNUTLS_TLS_STREAM_M@ OF_HTTP_CLIENT_TESTS_M = @OF_HTTP_CLIENT_TESTS_M@ OF_KQUEUE_KERNEL_EVENT_OBSERVER_M = @OF_KQUEUE_KERNEL_EVENT_OBSERVER_M@ +OF_MBEDTLS_TLS_STREAM_M = @OF_MBEDTLS_TLS_STREAM_M@ OF_OPENSSL_TLS_STREAM_M = @OF_OPENSSL_TLS_STREAM_M@ OF_POLL_KERNEL_EVENT_OBSERVER_M = @OF_POLL_KERNEL_EVENT_OBSERVER_M@ OF_SECURE_TRANSPORT_TLS_STREAM_M = @OF_SECURE_TRANSPORT_TLS_STREAM_M@ OF_SELECT_KERNEL_EVENT_OBSERVER_M = @OF_SELECT_KERNEL_EVENT_OBSERVER_M@ OF_SUBPROCESS_M = @OF_SUBPROCESS_M@ Index: src/OFTLSStream.h ================================================================== --- src/OFTLSStream.h +++ src/OFTLSStream.h @@ -112,10 +112,13 @@ /** * @brief Initializes the TLS stream with the specified stream as its * underlying stream. * + * @note The delegate of the specified stream will be changed to the TLS + * stream. You must not change this before the TLS session is completed. + * * @param stream The stream to use as underlying stream. Must not be closed * before the TLS stream is closed. * @return An initialized TLS stream */ - (instancetype)initWithStream: (OFStream + * + * All rights reserved. + * + * This file is part of ObjFW. It may be distributed under the terms of the + * Q Public License 1.0, which can be found in the file LICENSE.QPL included in + * the packaging of this file. + * + * Alternatively, it may be distributed under the terms of the GNU General + * Public License, either version 2 or 3, which can be found in the file + * LICENSE.GPLv2 or LICENSE.GPLv3 respectively included in the packaging of this + * file. + */ + +#import "OFTLSStream.h" + +#include + +OF_ASSUME_NONNULL_BEGIN + +@interface OFMbedTLSTLSStream: OFTLSStream +{ + bool _initialized, _handshakeDone; + mbedtls_ssl_config _config; + mbedtls_ssl_context _SSL; + OFString *_host; +} +@end + +OF_ASSUME_NONNULL_END ADDED src/tls/OFMbedTLSTLSStream.m Index: src/tls/OFMbedTLSTLSStream.m ================================================================== --- src/tls/OFMbedTLSTLSStream.m +++ src/tls/OFMbedTLSTLSStream.m @@ -0,0 +1,359 @@ +/* + * Copyright (c) 2008-2024 Jonathan Schleifer + * + * All rights reserved. + * + * This file is part of ObjFW. It may be distributed under the terms of the + * Q Public License 1.0, which can be found in the file LICENSE.QPL included in + * the packaging of this file. + * + * Alternatively, it may be distributed under the terms of the GNU General + * Public License, either version 2 or 3, which can be found in the file + * LICENSE.GPLv2 or LICENSE.GPLv3 respectively included in the packaging of this + * file. + */ + +#include "config.h" + +#include + +#import "OFMbedTLSTLSStream.h" +#import "OFData.h" + +#import "OFAlreadyOpenException.h" +#import "OFInitializationFailedException.h" +#import "OFNotOpenException.h" +#import "OFOutOfRangeException.h" +#import "OFReadFailedException.h" +#import "OFTLSHandshakeFailedException.h" +#import "OFWriteFailedException.h" + +#include +#include + +int _ObjFWTLS_reference; +static mbedtls_entropy_context entropy; +static mbedtls_ctr_drbg_context CTRDRBG; +static mbedtls_x509_crt CAChain; + +@implementation OFMbedTLSTLSStream +static int +readFunc(void *ctx, unsigned char *buffer, size_t length) +{ + OFMbedTLSTLSStream *stream = (OFMbedTLSTLSStream *)ctx; + + @try { + length = [stream.underlyingStream readIntoBuffer: buffer + length: length]; + } @catch (OFReadFailedException *e) { + return -1; + } + + if (length == 0 && !stream.underlyingStream.atEndOfStream) + return MBEDTLS_ERR_SSL_WANT_READ; + + if (length > INT_MAX) + @throw [OFOutOfRangeException exception]; + + return (int)length; +} + +static int +writeFunc(void *ctx, const unsigned char *buffer, size_t length) +{ + OFMbedTLSTLSStream *stream = (OFMbedTLSTLSStream *)ctx; + + @try { + [stream.underlyingStream writeBuffer: buffer length: length]; + } @catch (OFWriteFailedException *e) { + if (e.errNo == EWOULDBLOCK || e.errNo == EAGAIN) { + size_t bytesWritten = e.bytesWritten; + + if (bytesWritten > INT_MAX) + @throw [OFOutOfRangeException exception]; + + return (bytesWritten > 0 + ? (int)bytesWritten : MBEDTLS_ERR_SSL_WANT_WRITE); + } + + return -1; + } + + if (length > INT_MAX) + @throw [OFOutOfRangeException exception]; + + return (int)length; +} + ++ (void)load +{ + if (OFTLSStreamImplementation == Nil) + OFTLSStreamImplementation = self; +} + ++ (void)initialize +{ + if (self != [OFMbedTLSTLSStream class]) + return; + + mbedtls_entropy_init(&entropy); + if (mbedtls_ctr_drbg_seed(&CTRDRBG, mbedtls_entropy_func, &entropy, + NULL, 0) != 0) + @throw [OFInitializationFailedException + exceptionWithClass: self]; + + mbedtls_x509_crt_init(&CAChain); + if (mbedtls_x509_crt_parse_file(&CAChain, OF_MBEDTLS_CA_PATH) != 0) + @throw [OFInitializationFailedException + exceptionWithClass: self]; +} + +- (instancetype)initWithStream: (OFStream *)stream +{ + self = [super initWithStream: stream]; + + @try { + _underlyingStream.delegate = self; + } @catch (id e) { + [self release]; + @throw e; + } + + return self; +} + +- (void)dealloc +{ + if (_initialized) + [self close]; + + [_host release]; + + [super dealloc]; +} + +- (void)close +{ + if (!_initialized) + @throw [OFNotOpenException exceptionWithObject: self]; + + if (_handshakeDone) + mbedtls_ssl_close_notify(&_SSL); + + mbedtls_ssl_free(&_SSL); + mbedtls_ssl_config_free(&_config); + _initialized = _handshakeDone = false; + + [_host release]; + _host = nil; + + [super close]; +} + +- (size_t)lowlevelReadIntoBuffer: (void *)buffer length: (size_t)length +{ + int ret; + + if (!_handshakeDone) + @throw [OFNotOpenException exceptionWithObject: self]; + + if ((ret = mbedtls_ssl_read(&_SSL, buffer, length)) < 0) { + /* + * The underlying stream might have had data ready, but not + * enough for MbedTLS to return decrypted data. This means the + * caller might have observed the TLS stream for reading, got a + * ready signal and read - and expects the read to succeed, not + * to fail with EWOULDBLOCK/EAGAIN, as it was signaled ready. + * Therefore, return 0, as we could read 0 decrypted bytes, but + * cleared the ready signal of the underlying stream. + */ + if (ret == MBEDTLS_ERR_SSL_WANT_READ || + ret == MBEDTLS_ERR_SSL_WANT_WRITE) + return 0; + + /* FIXME: Translate error to errNo */ + @throw [OFReadFailedException exceptionWithObject: self + requestedLength: length + errNo: 0]; + } + + return ret; +} + +- (size_t)lowlevelWriteBuffer: (const void *)buffer length: (size_t)length +{ + int ret; + + if (!_handshakeDone) + @throw [OFNotOpenException exceptionWithObject: self]; + + if ((ret = mbedtls_ssl_write(&_SSL, buffer, length)) < 0) { + if (ret == MBEDTLS_ERR_SSL_WANT_READ || + ret == MBEDTLS_ERR_SSL_WANT_WRITE) + return 0; + + /* FIXME: Translate error to errNo */ + @throw [OFWriteFailedException exceptionWithObject: self + requestedLength: length + bytesWritten: ret + errNo: 0]; + } + + return ret; +} + +- (bool)lowlevelHasDataInReadBuffer +{ + return (_underlyingStream.hasDataInReadBuffer || + mbedtls_ssl_get_bytes_avail(&_SSL)); +} + +- (void)asyncPerformClientHandshakeWithHost: (OFString *)host + runLoopMode: (OFRunLoopMode)runLoopMode +{ + static const OFTLSStreamErrorCode initFailedErrorCode = + OFTLSStreamErrorCodeInitializationFailed; + id exception = nil; + int status; + + if (_initialized) + @throw [OFAlreadyOpenException exceptionWithObject: self]; + + if (mbedtls_ssl_config_defaults(&_config, MBEDTLS_SSL_IS_CLIENT, + MBEDTLS_SSL_TRANSPORT_STREAM, MBEDTLS_SSL_PRESET_DEFAULT) != 0) + @throw [OFTLSHandshakeFailedException + exceptionWithStream: self + host: host + errorCode: initFailedErrorCode]; + + mbedtls_ssl_conf_rng(&_config, mbedtls_ctr_drbg_random, &CTRDRBG); + mbedtls_ssl_conf_authmode(&_config, (_verifiesCertificates + ? MBEDTLS_SSL_VERIFY_REQUIRED : MBEDTLS_SSL_VERIFY_NONE)); + mbedtls_ssl_conf_ca_chain(&_config, &CAChain, NULL); + + mbedtls_ssl_init(&_SSL); + if (mbedtls_ssl_setup(&_SSL, &_config) != 0) + @throw [OFTLSHandshakeFailedException + exceptionWithStream: self + host: host + errorCode: initFailedErrorCode]; + + mbedtls_ssl_set_bio(&_SSL, self, writeFunc, readFunc, NULL); + + _host = [host copy]; + + if (mbedtls_ssl_set_hostname(&_SSL, _host.UTF8String) != 0) + @throw [OFTLSHandshakeFailedException + exceptionWithStream: self + host: host + errorCode: initFailedErrorCode]; + + status = mbedtls_ssl_handshake(&_SSL); + + if (status == MBEDTLS_ERR_SSL_WANT_READ) { + [_underlyingStream asyncReadIntoBuffer: (void *)"" + length: 0 + runLoopMode: runLoopMode]; + [_delegate retain]; + return; + } else if (status == MBEDTLS_ERR_SSL_WANT_WRITE) { + [_underlyingStream asyncWriteData: [OFData data] + runLoopMode: runLoopMode]; + [_delegate retain]; + return; + } + + if (status == 0) + _handshakeDone = true; + else + /* FIXME: Map to better errors */ + exception = [OFTLSHandshakeFailedException + exceptionWithStream: self + host: host + errorCode: OFTLSStreamErrorCodeUnknown]; + + if ([_delegate respondsToSelector: + @selector(stream:didPerformClientHandshakeWithHost:exception:)]) + [_delegate stream: self + didPerformClientHandshakeWithHost: host + exception: exception]; +} + +- (bool)stream: (OFStream *)stream + didReadIntoBuffer: (void *)buffer + length: (size_t)length + exception: (id)exception +{ + if (exception == nil) { + int status = mbedtls_ssl_handshake_step(&_SSL); + + if (status == MBEDTLS_ERR_SSL_WANT_READ) + return true; + else if (status == MBEDTLS_ERR_SSL_WANT_WRITE) { + OFRunLoopMode runLoopMode = + [OFRunLoop currentRunLoop].currentMode; + [_underlyingStream asyncWriteData: [OFData data] + runLoopMode: runLoopMode]; + return false; + } + + if (status == 0) + _handshakeDone = true; + else + exception = [OFTLSHandshakeFailedException + exceptionWithStream: self + host: _host + errorCode: OFTLSStreamErrorCodeUnknown]; + } + + if ([_delegate respondsToSelector: + @selector(stream:didPerformClientHandshakeWithHost:exception:)]) + [_delegate stream: self + didPerformClientHandshakeWithHost: _host + exception: exception]; + + [_delegate release]; + + return false; +} + +- (OFData *)stream: (OFStream *)stream + didWriteData: (OFData *)data + bytesWritten: (size_t)bytesWritten + exception: (id)exception +{ + if (exception == nil) { + int status = mbedtls_ssl_handshake_step(&_SSL); + + if (status == MBEDTLS_ERR_SSL_WANT_WRITE) + return data; + else if (status == MBEDTLS_ERR_SSL_WANT_READ) { + OFRunLoopMode runLoopMode = + [OFRunLoop currentRunLoop].currentMode; + [_underlyingStream asyncReadIntoBuffer: (void *)"" + length: 0 + runLoopMode: runLoopMode]; + return nil; + } + + if (status == 0) + _handshakeDone = true; + else + exception = [OFTLSHandshakeFailedException + exceptionWithStream: self + host: _host + errorCode: OFTLSStreamErrorCodeUnknown]; + } + + if ([_delegate respondsToSelector: + @selector(stream:didPerformClientHandshakeWithHost:exception:)]) + [_delegate stream: self + didPerformClientHandshakeWithHost: _host + exception: exception]; + + [_delegate release]; + + return nil; +} +@end Index: src/tls/OFOpenSSLTLSStream.m ================================================================== --- src/tls/OFOpenSSLTLSStream.m +++ src/tls/OFOpenSSLTLSStream.m @@ -92,10 +92,12 @@ SSL_shutdown(_SSL); SSL_free(_SSL); _SSL = NULL; + _handshakeDone = false; + [_host release]; _host = nil; [super close]; } @@ -285,13 +287,12 @@ length: bufferSize runLoopMode: runLoopMode]; [_delegate retain]; return; case SSL_ERROR_WANT_WRITE: - [_underlyingStream - asyncWriteData: [OFData dataWithItems: "" count: 0] - runLoopMode: runLoopMode]; + [_underlyingStream asyncWriteData: [OFData data] + runLoopMode: runLoopMode]; [_delegate retain]; return; default: /* FIXME: Map to better errors */ exception = [OFTLSHandshakeFailedException @@ -340,14 +341,13 @@ else { switch (SSL_get_error(_SSL, status)) { case SSL_ERROR_WANT_READ: return true; case SSL_ERROR_WANT_WRITE: - data = [OFData dataWithItems: "" count: 0]; OFRunLoopMode runLoopMode = [OFRunLoop currentRunLoop].currentMode; - [_underlyingStream asyncWriteData: data + [_underlyingStream asyncWriteData: [OFData data] runLoopMode: runLoopMode]; return false; default: exception = [OFTLSHandshakeFailedException exceptionWithStream: self Index: src/tls/OFSecureTransportTLSStream.m ================================================================== --- src/tls/OFSecureTransportTLSStream.m +++ src/tls/OFSecureTransportTLSStream.m @@ -125,10 +125,11 @@ CFRelease(_context); #else SSLDisposeContext(_context); #endif _context = NULL; + _handshakeDone = false; [super close]; } - (size_t)lowlevelReadIntoBuffer: (void *)buffer length: (size_t)length