/*
* 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