Index: .github/workflows/ubuntu-18.04-gcc.yml ================================================================== --- .github/workflows/ubuntu-18.04-gcc.yml +++ .github/workflows/ubuntu-18.04-gcc.yml @@ -17,13 +17,15 @@ - --disable-sockets --disable-files - --disable-files - --disable-shared - --disable-shared --enable-seluid24 - --disable-compiler-tls --disable-threads + - --with-tls=gnutls + - --with-tls=gnutls --disable-shared steps: - name: Install dependencies - run: sudo apt install gobjc gnutls-dev + run: sudo apt install gobjc openssl-dev gnutls-dev - uses: actions/checkout@v2 - name: autogen.sh run: ./autogen.sh - name: configure run: ./configure OBJC="gcc" ${{ matrix.configure_flags }} Index: .github/workflows/ubuntu-18.04.yml ================================================================== --- .github/workflows/ubuntu-18.04.yml +++ .github/workflows/ubuntu-18.04.yml @@ -17,13 +17,15 @@ - --disable-sockets --disable-files - --disable-files - --disable-shared - --disable-shared --enable-seluid24 - --disable-compiler-tls --disable-threads + - --with-tls=gnutls + - --with-tls=gnutls --disable-shared steps: - name: Install dependencies - run: sudo apt install gnutls-dev + run: sudo apt install openssl-dev gnutls-dev - uses: actions/checkout@v2 - name: autogen.sh run: ./autogen.sh - name: configure run: ./configure ${{ matrix.configure_flags }} Index: .github/workflows/ubuntu-20.04-gcc.yml ================================================================== --- .github/workflows/ubuntu-20.04-gcc.yml +++ .github/workflows/ubuntu-20.04-gcc.yml @@ -17,13 +17,15 @@ - --disable-sockets --disable-files - --disable-files - --disable-shared - --disable-shared --enable-seluid24 - --disable-compiler-tls --disable-threads + - --with-tls=gnutls + - --with-tls=gnutls --disable-shared steps: - name: Install dependencies - run: sudo apt install gobjc gnutls-dev + run: sudo apt install gobjc openssl-dev gnutls-dev - uses: actions/checkout@v2 - name: autogen.sh run: ./autogen.sh - name: configure run: ./configure OBJC="gcc" ${{ matrix.configure_flags }} Index: .github/workflows/ubuntu-20.04.yml ================================================================== --- .github/workflows/ubuntu-20.04.yml +++ .github/workflows/ubuntu-20.04.yml @@ -17,13 +17,15 @@ - --disable-sockets --disable-files - --disable-files - --disable-shared - --disable-shared --enable-seluid24 - --disable-compiler-tls --disable-threads + - --with-tls=gnutls + - --with-tls=gnutls --disable-shared steps: - name: Install dependencies - run: sudo apt install gnutls-dev + run: sudo apt install openssl-dev gnutls-dev - uses: actions/checkout@v2 - name: autogen.sh run: ./autogen.sh - name: configure run: ./configure ${{ matrix.configure_flags }} Index: configure.ac ================================================================== --- configure.ac +++ configure.ac @@ -1542,11 +1542,11 @@ esac AC_ARG_WITH(tls, AS_HELP_STRING([--with-tls], [ enable TLS support using the specified library - (yes, gnutls, securetransport or no)])) + (yes, openssl, gnutls, securetransport 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"], [ @@ -1569,10 +1569,26 @@ ], []) LIBS="$old_LIBS" ]) ]) + + AS_IF([test x"$with_tls" = x"openssl" \ + -o \( x"$with_tls" = x"yes" -a x"$tls_support" = x"no" \)], [ + AC_CHECK_LIB(ssl, SSL_set1_host, [ + AC_CHECK_HEADER(openssl/ssl.h, [ + AC_DEFINE(HAVE_OPENSSL, 1, + [Whether we have OpenSSL]) + + tls_support="OpenSSL" + TLS_LIBS="-lssl -lcrypto $TLS_LIBS" + + AC_SUBST(OF_OPENSSL_TLS_STREAM_M, + "OFOpenSSLTLSStream.m") + ]) + ], [], [-lcrypto]) + ]) AS_IF([test x"$with_tls" = x"gnutls" \ -o \( x"$with_tls" = x"yes" -a x"$tls_support" = x"no" \)], [ PKG_CHECK_MODULES(gnutls, [gnutls >= 3.5.0], [ AC_DEFINE(HAVE_GNUTLS, 1, [Whether we have GnuTLS]) @@ -1612,12 +1628,12 @@ ]) ]) AS_IF([test x"$with_tls" != x"no" -a x"$tls_support" = x"no"], [ AC_MSG_ERROR(m4_normalize([ - No TLS implementation was found. Please install GnuTLS - or use --without-tls. + No TLS implementation was found. Please install OpenSSL, + GnuTLS or use --without-tls. ])) ]) AS_IF([test x"$enable_threads" != x"no"], [ AC_SUBST(OF_HTTP_CLIENT_TESTS_M, "OFHTTPClientTests.m") Index: extra.mk.in ================================================================== --- extra.mk.in +++ extra.mk.in @@ -51,10 +51,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_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@ REEXPORT_RUNTIME = @REEXPORT_RUNTIME@ Index: src/tls/Makefile ================================================================== --- src/tls/Makefile +++ src/tls/Makefile @@ -8,10 +8,11 @@ LIB_MAJOR = ${OBJFW_LIB_MAJOR} LIB_MINOR = ${OBJFW_LIB_MINOR} INCLUDES := ObjFWTLS.h SRCS = ${OF_GNUTLS_TLS_STREAM_M} \ + ${OF_OPENSSL_TLS_STREAM_M} \ ${OF_SECURE_TRANSPORT_TLS_STREAM_M} includesubdir = ObjFWTLS include ../../buildsys.mk Index: src/tls/OFGnuTLSTLSStream.m ================================================================== --- src/tls/OFGnuTLSTLSStream.m +++ src/tls/OFGnuTLSTLSStream.m @@ -243,27 +243,27 @@ if (gnutls_record_get_direction(_session) == 1) [_underlyingStream asyncWriteData: [OFData dataWithItems: "" count: 0] runLoopMode: runLoopMode]; else - [_underlyingStream asyncReadIntoBuffer: (void *)"" + [_underlyingStream asyncReadIntoBuffer: (void *)"" length: 0 runLoopMode: runLoopMode]; [_delegate retain]; return; } - if (status != GNUTLS_E_SUCCESS) + if (status == GNUTLS_E_SUCCESS) + _handshakeDone = true; + else /* 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]; @@ -289,17 +289,17 @@ return false; } else return true; } - if (status != GNUTLS_E_SUCCESS) + if (status == GNUTLS_E_SUCCESS) + _handshakeDone = true; + else exception = [OFTLSHandshakeFailedException exceptionWithStream: self host: _host errorCode: OFTLSStreamErrorCodeUnknown]; - - _handshakeDone = true; } if ([_delegate respondsToSelector: @selector(stream:didPerformClientHandshakeWithHost:exception:)]) [_delegate stream: self @@ -325,24 +325,24 @@ return data; else { OFRunLoopMode runLoopMode = [OFRunLoop currentRunLoop].currentMode; [_underlyingStream - asyncReadIntoBuffer: (void *)"" + asyncReadIntoBuffer: (void *)"" length: 0 runLoopMode: runLoopMode]; return nil; } } - if (status != GNUTLS_E_SUCCESS) + if (status == GNUTLS_E_SUCCESS) + _handshakeDone = true; + else exception = [OFTLSHandshakeFailedException exceptionWithStream: self host: _host errorCode: OFTLSStreamErrorCodeUnknown]; - - _handshakeDone = true; } if ([_delegate respondsToSelector: @selector(stream:didPerformClientHandshakeWithHost:exception:)]) [_delegate stream: self ADDED src/tls/OFOpenSSLTLSStream.h Index: src/tls/OFOpenSSLTLSStream.h ================================================================== --- src/tls/OFOpenSSLTLSStream.h +++ src/tls/OFOpenSSLTLSStream.h @@ -0,0 +1,35 @@ +/* + * 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 +#include + +OF_ASSUME_NONNULL_BEGIN + +#define OFOpenSSLTLSStreamBufferSize 512 + +@interface OFOpenSSLTLSStream: OFTLSStream +{ + bool _handshakeDone; + SSL *_SSL; + BIO *_readBIO, *_writeBIO; + OFString *_host; + char _buffer[OFOpenSSLTLSStreamBufferSize]; +} +@end + +OF_ASSUME_NONNULL_END ADDED src/tls/OFOpenSSLTLSStream.m Index: src/tls/OFOpenSSLTLSStream.m ================================================================== --- src/tls/OFOpenSSLTLSStream.m +++ src/tls/OFOpenSSLTLSStream.m @@ -0,0 +1,417 @@ +/* + * 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 "OFOpenSSLTLSStream.h" +#import "OFData.h" + +#import "OFAlreadyConnectedException.h" +#import "OFInitializationFailedException.h" +#import "OFNotOpenException.h" +#import "OFReadFailedException.h" +#import "OFTLSHandshakeFailedException.h" +#import "OFWriteFailedException.h" + +#define bufferSize OFOpenSSLTLSStreamBufferSize + +int _ObjFWTLS_reference; +static SSL_CTX *clientContext; + +@implementation OFOpenSSLTLSStream ++ (void)load +{ + if (OFTLSStreamImplementation == Nil) + OFTLSStreamImplementation = self; +} + ++ (void)initialize +{ + if (self != [OFOpenSSLTLSStream class]) + return; + + SSL_load_error_strings(); + SSL_library_init(); + + if ((clientContext = SSL_CTX_new(TLS_client_method())) == NULL || + SSL_CTX_set_default_verify_paths(clientContext) != 1) + @throw [OFInitializationFailedException + exceptionWithClass: self]; +} + +- (instancetype)initWithStream: (OFStream *)stream +{ + self = [super initWithStream: stream]; + + @try { + _underlyingStream.delegate = self; + /* + * Buffer writes so that nothing gets lost if we write more + * than the underlying stream can write. + */ + _underlyingStream.buffersWrites = true; + } @catch (id e) { + [self release]; + @throw e; + } + + return self; +} + +- (void)dealloc +{ + if (_SSL != NULL) + [self close]; + + [_host release]; + + [super dealloc]; +} + +- (void)close +{ + if (_SSL == NULL) + @throw [OFNotOpenException exceptionWithObject: self]; + + if (_handshakeDone) + SSL_shutdown(_SSL); + + SSL_free(_SSL); + _SSL = NULL; + + [_host release]; + _host = nil; + + [super close]; +} + +- (size_t)lowlevelReadIntoBuffer: (void *)buffer length: (size_t)length +{ + int ret; + size_t bytesRead; + + if (!_handshakeDone) + @throw [OFNotOpenException exceptionWithObject: self]; + + if (BIO_ctrl_pending(_readBIO) < 1) { + @try { + size_t tmp = [_underlyingStream + readIntoBuffer: _buffer + length: bufferSize]; + + OFEnsure(tmp <= INT_MAX); + /* Writing to a memory BIO must never fail. */ + OFEnsure(BIO_write(_readBIO, _buffer, (int)tmp) == + (int)tmp); + } @catch (OFReadFailedException *e) { + if (e.errNo != EWOULDBLOCK && e.errNo != EAGAIN) + @throw e; + } + } + + if ((ret = SSL_read_ex(_SSL, buffer, length, &bytesRead)) != 1) { + /* + * The underlying stream might have had data ready, but not + * enough for OpenSSL 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 (SSL_get_error(_SSL, ret) == SSL_ERROR_WANT_READ) + return 0; + + /* FIXME: Translate error to errNo */ + @throw [OFReadFailedException exceptionWithObject: self + requestedLength: length + errNo: 0]; + } + + return bytesRead; +} + +- (size_t)lowlevelWriteBuffer: (const void *)buffer length: (size_t)length +{ + int ret; + size_t bytesWritten; + + if (!_handshakeDone) + @throw [OFNotOpenException exceptionWithObject: self]; + + if ((ret = SSL_write_ex(_SSL, buffer, length, &bytesWritten)) != 1) { + /* FIXME: Translate error to errNo */ + int errNo = 0; + + if (SSL_get_error(_SSL, ret) == SSL_ERROR_WANT_WRITE) + return bytesWritten; + + @throw [OFWriteFailedException exceptionWithObject: self + requestedLength: length + bytesWritten: bytesWritten + errNo: errNo]; + } + + if (BIO_ctrl_pending(_writeBIO) > 0) { + int tmp = BIO_read(_writeBIO, _buffer, bufferSize); + + OFEnsure(tmp >= 0); + + [_underlyingStream writeBuffer: _buffer length: tmp]; + [_underlyingStream flushWriteBuffer]; + } + + return bytesWritten; +} + +- (bool)hasDataInReadBuffer +{ + if (SSL_has_pending(_SSL) || BIO_ctrl_pending(_readBIO) > 0) + return true; + + return super.hasDataInReadBuffer; +} + +- (void)asyncPerformClientHandshakeWithHost: (OFString *)host + runLoopMode: (OFRunLoopMode)runLoopMode +{ + static const OFTLSStreamErrorCode initFailedErrorCode = + OFTLSStreamErrorCodeInitializationFailed; + id exception = nil; + int status; + + if (_SSL != NULL) + @throw [OFAlreadyConnectedException exceptionWithSocket: self]; + + if ((_readBIO = BIO_new(BIO_s_mem())) == NULL) + @throw [OFTLSHandshakeFailedException + exceptionWithStream: self + host: host + errorCode: initFailedErrorCode]; + + if ((_writeBIO = BIO_new(BIO_s_mem())) == NULL) { + BIO_free(_readBIO); + @throw [OFTLSHandshakeFailedException + exceptionWithStream: self + host: host + errorCode: initFailedErrorCode]; + } + + BIO_set_mem_eof_return(_readBIO, -1); + BIO_set_mem_eof_return(_writeBIO, -1); + + if ((_SSL = SSL_new(clientContext)) == NULL) { + BIO_free(_readBIO); + BIO_free(_writeBIO); + @throw [OFTLSHandshakeFailedException + exceptionWithStream: self + host: host + errorCode: initFailedErrorCode]; + } + + SSL_set_bio(_SSL, _readBIO, _writeBIO); + SSL_set_connect_state(_SSL); + + _host = [host copy]; + + if (SSL_set_tlsext_host_name(_SSL, _host.UTF8String) != 1) + @throw [OFTLSHandshakeFailedException + exceptionWithStream: self + host: host + errorCode: initFailedErrorCode]; + + if (_verifiesCertificates) { + SSL_set_verify(_SSL, SSL_VERIFY_PEER, NULL); + + if (SSL_set1_host(_SSL, _host.UTF8String) != 1) + @throw [OFTLSHandshakeFailedException + exceptionWithStream: self + host: host + errorCode: initFailedErrorCode]; + } + + status = SSL_do_handshake(_SSL); + + if (BIO_ctrl_pending(_writeBIO) > 0) { + int tmp = BIO_read(_writeBIO, _buffer, bufferSize); + + OFEnsure(tmp >= 0); + + [_underlyingStream writeBuffer: _buffer length: tmp]; + [_underlyingStream flushWriteBuffer]; + } + + if (status == 1) + _handshakeDone = true; + else { + switch (SSL_get_error(_SSL, status)) { + case SSL_ERROR_WANT_READ: + [_underlyingStream asyncReadIntoBuffer: _buffer + length: bufferSize + runLoopMode: runLoopMode]; + [_delegate retain]; + return; + case SSL_ERROR_WANT_WRITE: + [_underlyingStream + asyncWriteData: [OFData dataWithItems: "" count: 0] + runLoopMode: runLoopMode]; + [_delegate retain]; + return; + default: + /* FIXME: Map to better errors */ + exception = [OFTLSHandshakeFailedException + exceptionWithStream: self + host: host + errorCode: OFTLSStreamErrorCodeUnknown]; + break; + } + } + + 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) { + static const OFTLSStreamErrorCode unknownErrorCode = + OFTLSStreamErrorCodeUnknown; + int status; + OFData *data; + + OFEnsure(length <= INT_MAX); + OFEnsure(BIO_write(_readBIO, buffer, (int)length) == + (int)length); + + status = SSL_do_handshake(_SSL); + + if (BIO_ctrl_pending(_writeBIO) > 0) { + int tmp = BIO_read(_writeBIO, buffer, bufferSize); + + OFEnsure(tmp >= 0); + + [_underlyingStream writeBuffer: _buffer length: tmp]; + [_underlyingStream flushWriteBuffer]; + } + + if (status == 1) + _handshakeDone = true; + 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 + runLoopMode: runLoopMode]; + return false; + default: + exception = [OFTLSHandshakeFailedException + exceptionWithStream: self + host: _host + errorCode: unknownErrorCode]; + break; + } + } + } + + 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) { + static const OFTLSStreamErrorCode unknownErrorCode = + OFTLSStreamErrorCodeUnknown; + int status; + OFRunLoopMode runLoopMode; + + if (BIO_ctrl_pending(_writeBIO) > 0) { + int tmp = BIO_read(_writeBIO, _buffer, bufferSize); + + OFEnsure(tmp >= 0); + + [_underlyingStream writeBuffer: _buffer length: tmp]; + [_underlyingStream flushWriteBuffer]; + } + + status = SSL_do_handshake(_SSL); + + if (BIO_ctrl_pending(_writeBIO) > 0) { + int tmp = BIO_read(_writeBIO, _buffer, bufferSize); + + OFEnsure(tmp >= 0); + + [_underlyingStream writeBuffer: _buffer length: tmp]; + [_underlyingStream flushWriteBuffer]; + } + + if (status == 1) + _handshakeDone = true; + else { + switch (SSL_get_error(_SSL, status)) { + case SSL_ERROR_WANT_READ: + runLoopMode = + [OFRunLoop currentRunLoop].currentMode; + [_underlyingStream + asyncReadIntoBuffer: _buffer + length: bufferSize + runLoopMode: runLoopMode]; + return nil; + case SSL_ERROR_WANT_WRITE: + return data; + default: + exception = [OFTLSHandshakeFailedException + exceptionWithStream: self + host: _host + errorCode: unknownErrorCode]; + break; + } + } + } + + if ([_delegate respondsToSelector: + @selector(stream:didPerformClientHandshakeWithHost:exception:)]) + [_delegate stream: self + didPerformClientHandshakeWithHost: _host + exception: exception]; + + [_delegate release]; + + return nil; +} +@end