Index: configure.ac ================================================================== --- configure.ac +++ configure.ac @@ -1543,23 +1543,40 @@ AC_CHECK_FUNC(SSLHandshake, [ AC_DEFINE(HAVE_SECURE_TRANSPORT, 1, [Whether we have Secure Transport]) - tls_support="securetransport" + tls_support="Secure Transport" TLS_LIBS="-framework Foundation $TLS_LIBS" TLS_LIBS="-framework Security $TLS_LIBS" AC_SUBST(OF_SECURE_TRANSPORT_TLS_STREAM_M, "OFSecureTransportTLSStream.m") ], []) LIBS="$old_LIBS" ]) + + AS_IF([test x"$tls_support" = x"no"], [ + PKG_CHECK_MODULES(gnutls, [gnutls >= 3.5.0], [ + AC_DEFINE(HAVE_GNUTLS, 1, [Whether we have GnuTLS]) + + tls_support="GnuTLS" + TLS_CPPFLAGS="$gnutls_CFLAGS $TLS_CPPFLAGS" + TLS_LIBS="$gnutls_LIBS $TLS_LIBS" + + AC_SUBST(OF_GNUTLS_TLS_STREAM_M, "OFGnuTLSTLSStream.m") + ], [ + dnl Disable default action-if-not-found, which exits + dnl configure with an error. + : + ]) + ]) AS_IF([test x"$tls_support" != x"no"], [ AC_SUBST(TLS, "tls") + AC_SUBST(TLS_CPPFLAGS) AC_SUBST(TLS_LIBS) AC_DEFINE(HAVE_TLS_SUPPORT, 1, [Whether we have an implementation for TLS]) AC_CONFIG_FILES(src/tls/Info.plist) Index: extra.mk.in ================================================================== --- extra.mk.in +++ extra.mk.in @@ -48,10 +48,11 @@ OFHASH = @OFHASH@ OFHTTP = @OFHTTP@ OFHTTP_LIBS = @OFHTTP_LIBS@ 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_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@ @@ -70,10 +71,11 @@ TESTPLUGIN = @TESTPLUGIN@ TESTPLUGIN_LIBS = @TESTPLUGIN_LIBS@ TESTS_LIBS = @TESTS_LIBS@ TESTS_STATIC_LIB = @TESTS_STATIC_LIB@ TLS = @TLS@ +TLS_CPPFLAGS = @TLS_CPPFLAGS@ TLS_LIBS = @TLS_LIBS@ UNICODE_M = @UNICODE_M@ USE_INCLUDES_ATOMIC = @USE_INCLUDES_ATOMIC@ USE_SRCS_FILES = @USE_SRCS_FILES@ USE_SRCS_IPX = @USE_SRCS_IPX@ Index: src/OFTLSStream.m ================================================================== --- src/OFTLSStream.m +++ src/OFTLSStream.m @@ -120,10 +120,18 @@ { [_wrappedStream release]; [super dealloc]; } + +- (void)close +{ + [_wrappedStream release]; + _wrappedStream = nil; + + [super close]; +} - (size_t)lowlevelReadIntoBuffer: (void *)buffer length: (size_t)length { OF_UNRECOGNIZED_SELECTOR } Index: src/tls/Makefile ================================================================== --- src/tls/Makefile +++ src/tls/Makefile @@ -7,15 +7,16 @@ FRAMEWORK = ${OBJFWTLS_FRAMEWORK} LIB_MAJOR = ${OBJFW_LIB_MAJOR} LIB_MINOR = ${OBJFW_LIB_MINOR} INCLUDES := ObjFWTLS.h -SRCS = ${OF_SECURE_TRANSPORT_TLS_STREAM_M} +SRCS = ${OF_GNUTLS_TLS_STREAM_M} \ + ${OF_SECURE_TRANSPORT_TLS_STREAM_M} includesubdir = ObjFWTLS include ../../buildsys.mk -CPPFLAGS += -I. -I.. -I../.. -I../exceptions +CPPFLAGS += -I. -I.. -I../.. -I../exceptions -I../runtime ${TLS_CPPFLAGS} LD = ${OBJC} FRAMEWORK_LIBS := ${TLS_LIBS} -F.. -framework ObjFW ${LIBS} LIBS := ${TLS_LIBS} -L.. -lobjfw ${LIBS} ADDED src/tls/OFGnuTLSTLSStream.h Index: src/tls/OFGnuTLSTLSStream.h ================================================================== --- src/tls/OFGnuTLSTLSStream.h +++ src/tls/OFGnuTLSTLSStream.h @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2008-2021 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. + */ + +#import "OFTLSStream.h" + +#include + +OF_ASSUME_NONNULL_BEGIN + +@interface OFGnuTLSTLSStream: OFTLSStream +{ + bool _initialized, _handshakeDone; + gnutls_session_t _session; + OFString *_host; +} +@end + +OF_ASSUME_NONNULL_END ADDED src/tls/OFGnuTLSTLSStream.m Index: src/tls/OFGnuTLSTLSStream.m ================================================================== --- src/tls/OFGnuTLSTLSStream.m +++ src/tls/OFGnuTLSTLSStream.m @@ -0,0 +1,353 @@ +/* + * Copyright (c) 2008-2021 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 "OFGnuTLSTLSStream.h" +#import "OFData.h" + +#import "OFAlreadyConnectedException.h" +#import "OFInitializationFailedException.h" +#import "OFNotOpenException.h" +#import "OFReadFailedException.h" +#import "OFTLSHandshakeFailedException.h" +#import "OFWriteFailedException.h" + +int _ObjFWTLS_reference; +static gnutls_certificate_credentials_t systemTrustCreds; + +@implementation OFGnuTLSTLSStream +static ssize_t +readFunc(gnutls_transport_ptr_t transport, void *buffer, size_t length) +{ + OFGnuTLSTLSStream *stream = (OFGnuTLSTLSStream *)transport; + + @try { + length = [stream.wrappedStream readIntoBuffer: buffer + length: length]; + } @catch (OFReadFailedException *e) { + gnutls_transport_set_errno(stream->_session, e.errNo); + return -1; + } + + if (length == 0 && !stream.wrappedStream.atEndOfStream) { + gnutls_transport_set_errno(stream->_session, EWOULDBLOCK); + return -1; + } + + return length; +} + +static ssize_t +writeFunc(gnutls_transport_ptr_t transport, const void *buffer, size_t length) +{ + OFGnuTLSTLSStream *stream = (OFGnuTLSTLSStream *)transport; + + @try { + [stream.wrappedStream writeBuffer: buffer length: length]; + } @catch (OFWriteFailedException *e) { + gnutls_transport_set_errno(stream->_session, e.errNo); + + if (e.errNo == EWOULDBLOCK) + return e.bytesWritten; + + return -1; + } + + return length; +} + ++ (void)load +{ + if (OFTLSStreamImplementation == Nil) + OFTLSStreamImplementation = self; +} + ++ (void)initialize +{ + if (self != [OFGnuTLSTLSStream class]) + return; + + if (gnutls_certificate_allocate_credentials(&systemTrustCreds) != + GNUTLS_E_SUCCESS || + gnutls_certificate_set_x509_system_trust(systemTrustCreds) < 0) + @throw [OFInitializationFailedException exception]; +} + +- (instancetype)initWithStream: (OFStream *)stream +{ + self = [super initWithStream: stream]; + + @try { + _wrappedStream.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) + gnutls_bye(_session, GNUTLS_SHUT_WR); + + gnutls_deinit(_session); + _initialized = false; + + [_host release]; + _host = nil; + + [super close]; +} + +- (size_t)lowlevelReadIntoBuffer: (void *)buffer length: (size_t)length +{ + ssize_t ret; + + if (!_handshakeDone) + @throw [OFNotOpenException exceptionWithObject: self]; + + if ((ret = gnutls_record_recv(_session, buffer, length)) < 0) { + /* + * The underlying stream might have had data ready, but not + * enough for GnuTLS 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, 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 == GNUTLS_E_INTERRUPTED || ret == GNUTLS_E_AGAIN) + 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 +{ + ssize_t ret; + + if (!_handshakeDone) + @throw [OFNotOpenException exceptionWithObject: self]; + + if ((ret = gnutls_record_send(_session, buffer, length)) < 0) { + /* FIXME: Translate error to errNo */ + int errNo = 0; + + if (ret == GNUTLS_E_INTERRUPTED || ret == GNUTLS_E_AGAIN) + errNo = EWOULDBLOCK; + + @throw [OFWriteFailedException exceptionWithObject: self + requestedLength: length + bytesWritten: ret + errNo: errNo]; + } + + return ret; +} + +- (bool)hasDataInReadBuffer +{ + if (gnutls_record_check_pending(_session) > 0) + return true; + + return super.hasDataInReadBuffer; +} + +- (void)asyncPerformClientHandshakeWithHost: (OFString *)host + runLoopMode: (OFRunLoopMode)runLoopMode +{ + static const OFTLSStreamErrorCode initFailedErrorCode = + OFTLSStreamErrorCodeInitializationFailed; + id exception = nil; + int status; + + if (_initialized) + @throw [OFAlreadyConnectedException exceptionWithSocket: self]; + + if (gnutls_init(&_session, GNUTLS_CLIENT | GNUTLS_NONBLOCK | + GNUTLS_SAFE_PADDING_CHECK) != GNUTLS_E_SUCCESS) + @throw [OFTLSHandshakeFailedException + exceptionWithStream: self + host: host + errorCode: initFailedErrorCode]; + + _initialized = true; + + gnutls_transport_set_ptr(_session, self); + gnutls_transport_set_pull_function(_session, readFunc); + gnutls_transport_set_push_function(_session, writeFunc); + + if (gnutls_set_default_priority(_session) != GNUTLS_E_SUCCESS || + gnutls_credentials_set(_session, GNUTLS_CRD_CERTIFICATE, + systemTrustCreds) != GNUTLS_E_SUCCESS) + @throw [OFTLSHandshakeFailedException + exceptionWithStream: self + host: host + errorCode: initFailedErrorCode]; + + _host = [host copy]; + + if (gnutls_server_name_set(_session, GNUTLS_NAME_DNS, + _host.UTF8String, _host.UTF8StringLength) != GNUTLS_E_SUCCESS) + @throw [OFTLSHandshakeFailedException + exceptionWithStream: self + host: host + errorCode: initFailedErrorCode]; + + if (_verifiesCertificates) + gnutls_session_set_verify_cert(_session, _host.UTF8String, 0); + + status = gnutls_handshake(_session); + + if (status == GNUTLS_E_INTERRUPTED || status == GNUTLS_E_AGAIN) { + if (gnutls_record_get_direction(_session) == 1) + [_wrappedStream + asyncWriteData: [OFData dataWithItems: "" count: 0] + runLoopMode: runLoopMode]; + else + [_wrappedStream asyncReadIntoBuffer: (void *)"" + length: 0 + runLoopMode: runLoopMode]; + + [_delegate retain]; + return; + } + + if (status != GNUTLS_E_SUCCESS) + /* FIXME: Map to better errors */ + exception = [OFTLSHandshakeFailedException + exceptionWithStream: self + host: host + errorCode: OFTLSStreamErrorCodeUnknown]; + + _handshakeDone = true; + + 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: (nullable id)exception +{ + if (exception == nil) { + int status = gnutls_handshake(_session); + + if (status == GNUTLS_E_INTERRUPTED || + status == GNUTLS_E_AGAIN) { + if (gnutls_record_get_direction(_session) == 1) { + OFData *data = [OFData dataWithItems: "" + count: 0]; + OFRunLoopMode runLoopMode = + [OFRunLoop currentRunLoop].currentMode; + [_wrappedStream asyncWriteData: data + runLoopMode: runLoopMode]; + return false; + } else + return true; + } + + if (status != GNUTLS_E_SUCCESS) + exception = [OFTLSHandshakeFailedException + exceptionWithStream: self + host: _host + errorCode: OFTLSStreamErrorCodeUnknown]; + + _handshakeDone = true; + } + + 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 = gnutls_handshake(_session); + + if (status == GNUTLS_E_INTERRUPTED || + status == GNUTLS_E_AGAIN) { + if (gnutls_record_get_direction(_session) == 1) + return data; + else { + OFRunLoopMode runLoopMode = + [OFRunLoop currentRunLoop].currentMode; + [_wrappedStream + asyncReadIntoBuffer: (void *)"" + length: 0 + runLoopMode: runLoopMode]; + return nil; + } + } + + if (status != GNUTLS_E_SUCCESS) + exception = [OFTLSHandshakeFailedException + exceptionWithStream: self + host: _host + errorCode: OFTLSStreamErrorCodeUnknown]; + + _handshakeDone = true; + } + + if ([_delegate respondsToSelector: + @selector(stream:didPerformClientHandshakeWithHost:exception:)]) + [_delegate stream: self + didPerformClientHandshakeWithHost: _host + exception: exception]; + + [_delegate release]; + + return nil; +} +@end Index: src/tls/OFSecureTransportTLSStream.h ================================================================== --- src/tls/OFSecureTransportTLSStream.h +++ src/tls/OFSecureTransportTLSStream.h @@ -15,15 +15,13 @@ #import "OFTLSStream.h" OF_ASSUME_NONNULL_BEGIN -extern int _OFSecureTransportTLSSocket_reference; - @interface OFSecureTransportTLSStream: OFTLSStream { struct SSLContext *_context; OFString *_host; } @end OF_ASSUME_NONNULL_END Index: src/tls/OFSecureTransportTLSStream.m ================================================================== --- src/tls/OFSecureTransportTLSStream.m +++ src/tls/OFSecureTransportTLSStream.m @@ -101,10 +101,13 @@ - (void)close { if (_context == NULL) @throw [OFNotOpenException exceptionWithObject: self]; + + [_host release]; + _host = nil; SSLClose(_context); CFRelease(_context); _context = NULL; @@ -130,24 +133,24 @@ } - (size_t)lowlevelWriteBuffer: (const void *)buffer length: (size_t)length { OSStatus status; - size_t ret = 0; + size_t bytesWritten = 0; if (_context == NULL) @throw [OFNotOpenException exceptionWithObject: self]; - status = SSLWrite(_context, buffer, length, &ret); + status = SSLWrite(_context, buffer, length, &bytesWritten); if (status != noErr && status != errSSLWouldBlock) /* FIXME: Translate status to errNo */ @throw [OFWriteFailedException exceptionWithObject: self requestedLength: length - bytesWritten: ret + bytesWritten: bytesWritten errNo: 0]; - return ret; + return bytesWritten; } - (bool)hasDataInReadBuffer { size_t bufferSize; @@ -181,17 +184,19 @@ SSLSetConnection(_context, self) != noErr) @throw [OFTLSHandshakeFailedException exceptionWithStream: self host: host errorCode: initFailedErrorCode]; + + _host = [host copy]; if (_verifiesCertificates) if (SSLSetPeerDomainName(_context, - host.UTF8String, host.UTF8StringLength) != noErr) + _host.UTF8String, _host.UTF8StringLength) != noErr) @throw [OFTLSHandshakeFailedException exceptionWithStream: self - host: host + host: _host errorCode: initFailedErrorCode]; status = SSLHandshake(_context); if (status == errSSLWouldBlock) { @@ -205,25 +210,24 @@ */ [_wrappedStream asyncReadIntoBuffer: (void *)"" length: 0 runLoopMode: runLoopMode]; [_delegate retain]; - _host = [host copy]; return; } if (status != noErr) /* FIXME: Map to better errors */ exception = [OFTLSHandshakeFailedException exceptionWithStream: self - host: host + host: _host errorCode: OFTLSStreamErrorCodeUnknown]; if ([_delegate respondsToSelector: @selector(stream:didPerformClientHandshakeWithHost:exception:)]) [_delegate stream: self - didPerformClientHandshakeWithHost: host + didPerformClientHandshakeWithHost: _host exception: exception]; } - (bool)stream: (OFStream *)stream didReadIntoBuffer: (void *)buffer @@ -247,13 +251,10 @@ @selector(stream:didPerformClientHandshakeWithHost:exception:)]) [_delegate stream: self didPerformClientHandshakeWithHost: _host exception: exception]; - [_host release]; - _host = nil; - [_delegate release]; return false; } @end