ObjOpenSSL  SSLSocket.m at [3f1ad168e1]

File src/SSLSocket.m artifact 18992aefde part of check-in 3f1ad168e1


/*
 * Copyright (c) 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020
 *     Jonathan Schleifer <js@nil.im>
 * Copyright (c) 2011, Florian Zeitz <florob@babelmonkeys.de>
 * Copyright (c) 2011, Jos Kuijpers <jos@kuijpersvof.nl>
 *
 * https://fossil.nil.im/objopenssl
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice is present in all copies.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

#include <unistd.h>
#include <errno.h>
#include <assert.h>

#if defined(__clang__)
# pragma clang diagnostic push
# pragma clang diagnostic ignored "-Wdocumentation"
#endif

#include <openssl/crypto.h>
#include <openssl/err.h>
#include <openssl/ssl.h>
#include <openssl/x509v3.h>

#if defined(__clang__)
# pragma clang diagnostic pop
#endif

#import <ObjFW/OFThread.h>
#import <ObjFW/OFHTTPRequest.h>
#import <ObjFW/OFData.h>
#import <ObjFW/OFLocale.h>

#import <ObjFW/OFAcceptFailedException.h>
#import <ObjFW/OFInitializationFailedException.h>
#import <ObjFW/OFInvalidArgumentException.h>
#import <ObjFW/OFNotOpenException.h>
#import <ObjFW/OFOutOfRangeException.h>
#import <ObjFW/OFReadFailedException.h>
#import <ObjFW/OFWriteFailedException.h>

#import <ObjFW/macros.h>
#import <ObjFW/mutex.h>

#import "SSLSocket.h"
#import "X509Certificate.h"

#import "SSLConnectionFailedException.h"
#import "SSLInvalidCertificateException.h"

#ifndef INVALID_SOCKET
# define INVALID_SOCKET -1
#endif

static SSL_CTX *ctx;
static of_mutex_t *ssl_mutexes;

static unsigned long
threadID(void)
{
	return (unsigned long)(uintptr_t)[OFThread currentThread];
}

static void
lockingCallback(int mode, int n, const char *file, int line)
{
	/*
	 * This function must handle up to CRYPTO_num_locks() mutexes.
	 * It must set the n-th lock if mode & CRYPTO_LOCK,
	 * release it otherwise.
	 */
	if (mode & CRYPTO_LOCK)
		of_mutex_lock(&ssl_mutexes[n]);
	else
		of_mutex_unlock(&ssl_mutexes[n]);
}

@interface SSLSocket ()
- (void)SSL_startTLSWithExpectedHost: (OFString *)host
				port: (uint16_t)port;
- (void)SSL_super_close;
@end

@interface SSLSocket_ConnectDelegate: OFObject <OFTLSSocketDelegate>
{
	SSLSocket *_socket;
	OFString *_host;
	uint16_t _port;
	id <OFTLSSocketDelegate> _delegate;
}

- (instancetype)initWithSocket: (SSLSocket *)sock
			  host: (OFString *)host
			  port: (uint16_t)port
		      delegate: (id <OFTLSSocketDelegate>)delegate;
@end

@implementation SSLSocket_ConnectDelegate
- (instancetype)initWithSocket: (SSLSocket *)sock
			  host: (OFString *)host
			  port: (uint16_t)port
		      delegate: (id <OFTLSSocketDelegate>)delegate
{
	self = [super init];

	@try {
		_socket = [sock retain];
		_host = [host copy];
		_port = port;
		_delegate = [delegate retain];

		_socket.delegate = self;
	} @catch (id e) {
		[self release];
		@throw e;
	}

	return self;
}

- (void)dealloc
{
	if (_socket.delegate == self)
		_socket.delegate = _delegate;

	[_socket release];
	[_delegate release];

	[super dealloc];
}

-     (void)socket: (OFTCPSocket *)sock
  didConnectToHost: (OFString *)host
	      port: (uint16_t)port
	 exception: (id)exception
{
	if (exception == nil) {
		@try {
			[(SSLSocket *)sock SSL_startTLSWithExpectedHost: _host
								   port: _port];
		} @catch (id e) {
			exception = e;
		}
	}

	_socket.delegate = _delegate;
	[_delegate    socket: sock
	    didConnectToHost: host
			port: port
		   exception: exception];
}
@end

@implementation SSLSocket
@dynamic delegate;
@synthesize certificateFile = _certificateFile;
@synthesize privateKeyFile = _privateKeyFile;
@synthesize privateKeyPassphrase = _privateKeyPassphrase;
@synthesize verifiesCertificates = _verifiesCertificates;
@synthesize requestsClientCertificates = _requestsClientCertificates;

+ (void)load
{
	of_tls_socket_class = self;
}

+ (void)initialize
{
	int m;

	if (self != [SSLSocket class])
		return;

	CRYPTO_set_id_callback(&threadID);
	/* OpenSSL >= 1.1 defines the line above to a nop */
	(void)threadID;

	/* Generate number of mutexes needed */
	m = CRYPTO_num_locks();
	ssl_mutexes = malloc(m * sizeof(of_mutex_t));
	for (m--; m >= 0; m--)
		of_mutex_new(&ssl_mutexes[m]);

	CRYPTO_set_locking_callback(&lockingCallback);
	/* OpenSSL >= 1.1 defines the line above to a nop */
	(void)lockingCallback;

	SSL_library_init();

	if ((ctx = SSL_CTX_new(SSLv23_method())) == NULL)
		@throw [OFInitializationFailedException
		    exceptionWithClass: self];

#if SSL_OP_NO_SSLv2 != 0
	if ((SSL_CTX_set_options(ctx, SSL_OP_NO_SSLv2) & SSL_OP_NO_SSLv2) == 0)
		@throw [OFInitializationFailedException
		    exceptionWithClass: self];
#endif

	if (SSL_CTX_set_default_verify_paths(ctx) == 0)
		@throw [OFInitializationFailedException
		    exceptionWithClass: self];
}

- (instancetype)init
{
	self = [super init];

	_verifiesCertificates = true;

	return self;
}

- (instancetype)initWithSocket: (OFTCPSocket *)socket
{
	self = [self init];

	@try {
		if ((_socket = dup(socket->_socket)) < 0)
			@throw [OFInitializationFailedException exception];
	} @catch (id e) {
		[self release];
		@throw e;
	}

	return self;
}

- (void)dealloc
{
	SSL *SSL_ = _SSL;

	[_privateKeyFile release];
	[_certificateFile release];

	[super dealloc];

	if (SSL_ != NULL)
		SSL_free(SSL_);
}

- (void)SSL_startTLSWithExpectedHost: (OFString *)host
				port: (uint16_t)port
{
	of_string_encoding_t encoding;

	if ((_SSL = SSL_new(ctx)) == NULL || SSL_set_fd(_SSL, _socket) != 1) {
		unsigned long error = ERR_get_error();

		[super close];

		@throw [SSLConnectionFailedException
		    exceptionWithHost: host
				 port: port
			       socket: self
			     SSLError: error];
	}

	if (SSL_set_tlsext_host_name(_SSL, host.UTF8String) != 1) {
		unsigned long error = ERR_get_error();

		[self close];

		@throw [SSLConnectionFailedException exceptionWithHost: host
								  port: port
								socket: self
							      SSLError: error];
	}

	if (_verifiesCertificates) {
		X509_VERIFY_PARAM *param = SSL_get0_param(_SSL);

		X509_VERIFY_PARAM_set_hostflags(param,
		    X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS);

		if (X509_VERIFY_PARAM_set1_host(param,
		    host.UTF8String, host.UTF8StringLength) != 1) {
			unsigned long error = ERR_get_error();

			[self close];

			@throw [SSLConnectionFailedException
			    exceptionWithHost: host
					 port: port
				       socket: self
				     SSLError: error];
		}

		SSL_set_verify(_SSL, SSL_VERIFY_PEER, NULL);
	}

	SSL_set_connect_state(_SSL);

	encoding = [OFLocale encoding];

	if ((_privateKeyFile != nil && !SSL_use_PrivateKey_file(_SSL,
	    [_privateKeyFile cStringWithEncoding: encoding],
	    SSL_FILETYPE_PEM)) || (_certificateFile != nil &&
	    !SSL_use_certificate_file(_SSL, [_certificateFile
	    cStringWithEncoding: encoding], SSL_FILETYPE_PEM))) {
		unsigned long error = ERR_get_error();

		[super close];

		@throw [SSLConnectionFailedException
		    exceptionWithHost: host
				 port: port
			       socket: self
			     SSLError: error];
	}

	if (SSL_connect(_SSL) != 1) {
		unsigned long error = ERR_get_error();
		long res;

		[super close];

		if ((res = SSL_get_verify_result(_SSL)) != X509_V_OK)
			@throw [SSLConnectionFailedException
			    exceptionWithHost: host
					 port: port
				       socket: self
				     SSLError: error
				 verifyResult: res];
		else
			@throw [SSLConnectionFailedException
			    exceptionWithHost: host
					 port: port
				       socket: self
				     SSLError: error];
	}
}

- (void)startTLSWithExpectedHost: (OFString *)host
{
	[self SSL_startTLSWithExpectedHost: host
				      port: 0];
}

- (void)asyncConnectToHost: (OFString *)host
		      port: (uint16_t)port
	       runLoopMode: (of_run_loop_mode_t)runLoopMode
{
	void *pool = objc_autoreleasePoolPush();

	[[[SSLSocket_ConnectDelegate alloc]
	    initWithSocket: self
		      host: host
		      port: port
		  delegate: _delegate] autorelease];
	[super asyncConnectToHost: host
			     port: port
		      runLoopMode: runLoopMode];

	objc_autoreleasePoolPop(pool);
}

#ifdef OF_HAVE_BLOCKS
- (void)asyncConnectToHost: (OFString *)host
		      port: (uint16_t)port
	       runLoopMode: (of_run_loop_mode_t)runLoopMode
		     block: (of_tcp_socket_async_connect_block_t)block
{
	[super asyncConnectToHost: host
			     port: port
		      runLoopMode: runLoopMode
			    block: ^ (id exception) {
		if (exception == nil) {
			@try {
				[self SSL_startTLSWithExpectedHost: host
							      port: port];
			} @catch (id e) {
				block(e);
				return;
			}
		}

		block(exception);
	}];
}
#endif

- (instancetype)accept
{
	SSLSocket *client = (SSLSocket *)[super accept];
	of_string_encoding_t encoding;

	if ((client->_SSL = SSL_new(ctx)) == NULL ||
	    !SSL_set_fd(client->_SSL, client->_socket)) {
		[client SSL_super_close];
		/* FIXME: Get a proper errno */
		@throw [OFAcceptFailedException exceptionWithSocket: self
							      errNo: 0];
	}

	if (_requestsClientCertificates)
		SSL_set_verify(client->_SSL, SSL_VERIFY_PEER, NULL);

	SSL_set_accept_state(client->_SSL);

	encoding = [OFLocale encoding];

	if (!SSL_use_PrivateKey_file(client->_SSL, [_privateKeyFile
	    cStringWithEncoding: encoding],
	    SSL_FILETYPE_PEM) || !SSL_use_certificate_file(client->_SSL,
	    [_certificateFile cStringWithEncoding: encoding],
	    SSL_FILETYPE_PEM) || SSL_accept(client->_SSL) != 1) {
		[client SSL_super_close];
		/* FIXME: Get a proper errno */
		@throw [OFAcceptFailedException exceptionWithSocket: self
							      errNo: 0];
	}

	return client;
}

- (void)close
{
	if (_SSL != NULL)
		SSL_shutdown(_SSL);

	[super close];
}

- (void)SSL_super_close
{
	[super close];
}

- (size_t)lowlevelReadIntoBuffer: (void *)buffer
			  length: (size_t)length
{
	ssize_t ret;

	/*
	 * There is no SSL session yet. However, it might be necessary to read
	 * from and write to the socket before negotiating an SSL session: For
	 * example, the socket might be connected to a SOCKS5 proxy and needs
	 * to establish a SOCKS5 connection before negotiating an SSL session.
	 */
	if (_SSL == NULL)
		return [super lowlevelReadIntoBuffer: buffer
					      length: length];

	if (length > INT_MAX)
		@throw [OFOutOfRangeException exception];

	if (_socket == INVALID_SOCKET)
		@throw [OFNotOpenException exceptionWithObject: self];

	if (_atEndOfStream)
		@throw [OFReadFailedException exceptionWithObject: self
						  requestedLength: length
							    errNo: ENOTCONN];

	if ((ret = SSL_read(_SSL, buffer, (int)length)) < 0) {
		if (SSL_get_error(_SSL, ret) == SSL_ERROR_WANT_READ)
			return 0;

		@throw [OFReadFailedException exceptionWithObject: self
						  requestedLength: length
							    errNo: 0];
	}

	if (ret == 0)
		_atEndOfStream = true;

	return ret;
}

- (size_t)lowlevelWriteBuffer: (const void *)buffer
		       length: (size_t)length
{
	int bytesWritten;

	/*
	 * There is no SSL session yet. However, it might be necessary to read
	 * from and write to the socket before negotiating an SSL session: For
	 * example, the socket might be connected to a SOCKS5 proxy and needs
	 * to establish a SOCKS5 connection before negotiating an SSL session.
	 *
	 * TODO: Think of a way to make this safer, so that it's impossible to
	 * forget to establish an SSL session and then send unencrypted data by
	 * accident.
	 */
	if (_SSL == NULL)
		return [super lowlevelWriteBuffer: buffer
					   length: length];

	if (_socket == INVALID_SOCKET)
		@throw [OFNotOpenException exceptionWithObject: self];

	if (length > INT_MAX)
		@throw [OFOutOfRangeException exception];

	if ((bytesWritten = SSL_write(_SSL, buffer, (int)length)) < 0)
		@throw [OFWriteFailedException exceptionWithObject: self
						   requestedLength: length
						      bytesWritten: 0
							     errNo: 0];

	return bytesWritten;
}

- (bool)hasDataInReadBuffer
{
	if (_SSL != NULL && SSL_pending(_SSL) > 0)
		return true;

	return super.hasDataInReadBuffer;
}

- (void)setCertificateFile: (OFString *)certificateFile
		forSNIHost: (OFString *)SNIHost
{
	/* TODO */
	OF_UNRECOGNIZED_SELECTOR
}

- (OFString *)certificateFileForSNIHost: (OFString *)SNIHost
{
	/* TODO */
	OF_UNRECOGNIZED_SELECTOR
}

- (void)setPrivateKeyFile: (OFString *)privateKeyFile
	       forSNIHost: (OFString *)SNIHost
{
	/* TODO */
	OF_UNRECOGNIZED_SELECTOR
}

- (OFString *)privateKeyFileForSNIHost: (OFString *)SNIHost
{
	/* TODO */
	OF_UNRECOGNIZED_SELECTOR
}

- (void)setPrivateKeyPassphrase: (const char *)privateKeyPassphrase
		     forSNIHost: (OFString *)SNIHost
{
	/* TODO */
	OF_UNRECOGNIZED_SELECTOR
}

- (const char *)privateKeyPassphraseForSNIHost: (OFString *)SNIHost
{
	/* TODO */
	OF_UNRECOGNIZED_SELECTOR
}

- (OFData *)channelBindingDataWithType: (OFString *)type
{
	size_t length;
	char buffer[64];

	if (![type isEqual: @"tls-unique"])
		@throw [OFInvalidArgumentException exception];

	if (SSL_session_reused(_SSL) ^ !_listening) {
		/*
		 * We are either client or the session has been resumed
		 * => we have sent the finished message
		 */
		length = SSL_get_finished(_SSL, buffer, 64);
	} else {
		/* peer sent the finished message */
		length = SSL_get_peer_finished(_SSL, buffer, 64);
	}

	return [OFData dataWithItems: buffer
			       count: length];
}

- (X509Certificate *)peerCertificate
{
	X509 *certificate = SSL_get_peer_certificate(_SSL);

	if (certificate == NULL)
		return nil;

	return [[[X509Certificate alloc]
	    initWithX509Struct: certificate] autorelease];
}

- (void)verifyPeerCertificate
{
	unsigned long ret;

	if (SSL_get_peer_certificate(_SSL) != NULL) {
		if ((ret = SSL_get_verify_result(_SSL)) != X509_V_OK) {
			const char *tmp = X509_verify_cert_error_string(ret);
			OFString *reason = [OFString stringWithUTF8String: tmp];
			@throw [SSLInvalidCertificateException
			    exceptionWithReason: reason];
		}
	} else
		@throw [SSLInvalidCertificateException
		    exceptionWithReason: @"No certificate"];
}
@end