unit_tests.m   [plain text]


//
//  unit_tests.m
//  openssl
//
//  Created by J. Osborne on 1/25/10.
//  Copyright 2010 Apple. All rights reserved.
//

#import "unit_tests.h"

#include <stdarg.h>
#include <err.h>
#include <spawn.h>
#include <openssl/bio.h>
#include <openssl/ssl.h>
#include <openssl/err.h>
#include <crt_externs.h>

static char *client_pem, *signed_pem, *url;
static int port = 9001;

@implementation unit_tests

void (^comm_test)(BOOL cert_fail, BOOL handshake_fail) = NULL;
void (^comm_teardown)(void) = NULL;

static int verify_callback(int preverify_ok, X509_STORE_CTX *ctx)
{
	fprintf(stderr, "Peer certificate verification callback fired, ctx->error %d, preverify_ok %d\n", ctx->error, preverify_ok);
	return 0;
}

void comm_setup(id self)
{
	NSBundle* myBundle = [NSBundle bundleWithIdentifier:@"com.apple.OpenSSL098-unit-tests"];
	
	char *cert_file = strdup([[myBundle pathForResource:@"server" ofType:@"pem"] UTF8String]);
	char *priv_key_file = strdup([[myBundle pathForResource:@"server" ofType:@"pem"] UTF8String]);
	client_pem = strdup([[myBundle pathForResource:@"client" ofType:@"pem"] UTF8String]);
	signed_pem = strdup([[myBundle pathForResource:@"signed" ofType:@"pem"] UTF8String]);
	
	STAssertTrue(NULL != cert_file, @"Can't setup test, missing cert_file");
	STAssertTrue(NULL != priv_key_file, @"Can't setup test, missing priv_key_file");
	STAssertTrue(NULL != client_pem, @"Can't setup test, missing client_pem");
	STAssertTrue(NULL != signed_pem, @"Can't setup test, missing signed_pem");

	SSL_library_init();
	SSL_load_error_strings();
	ERR_load_BIO_strings();
	ERR_load_SSL_strings();
	
	__block SSL_CTX *ctx = SSL_CTX_new(SSLv23_server_method());
	if (ctx == NULL) {
		STFail(@"Failed to create SSL context");
		return;
	}
	if (!SSL_CTX_use_certificate_file(ctx, cert_file, SSL_FILETYPE_PEM)) {
		STFail(@"Failed to load certificate file %s", cert_file);
		// if it is important to get these errors into the "real" failure log, look at funopen
		ERR_print_errors_fp(stderr);
		SSL_CTX_free(ctx);
		return;
	}
	if (!SSL_CTX_use_PrivateKey_file(ctx, priv_key_file, SSL_FILETYPE_PEM))
	{
		STFail(@"Failed to load private key file %s", priv_key_file);
		ERR_print_errors_fp(stdout);
		SSL_CTX_free(ctx);
		return;
	}
	// at this point we have loaded the certificate and private key
	
	SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, verify_callback);
	
	__block BIO *bio = BIO_new_ssl(ctx, 0);
	if (bio == NULL)
	{
		STFail(@"Failed to acquire I/O chain for SSL context");
		ERR_print_errors_fp(stdout);
		SSL_CTX_free(ctx);
		return;
	}
	
	char *portstr;
	asprintf(&portstr, "%d", port);
	asprintf(&url, "https://127.0.0.1:%d", port);
	__block BIO *abio = BIO_new_accept(portstr);
	BIO_set_bind_mode(abio, BIO_BIND_REUSEADDR);
	free(portstr);

	BIO_set_accept_bios(abio, bio);
	if (BIO_do_accept(abio) <= 0)
	{
		STFail(@"Failed to listen on port %d", port);
		ERR_print_errors_fp(stdout);
		SSL_CTX_free(ctx);
		BIO_free_all(bio);
		BIO_free_all(abio);
		return;
	}
	
	comm_test = ^(BOOL cert_fail, BOOL handshake_fail) {
		if (BIO_do_accept(abio) <= 0)
		{
			STFail(@"Failed to accept a connection");
			ERR_print_errors_fp(stdout);
			SSL_CTX_free(ctx);
			BIO_free_all(bio);
			BIO_free_all(abio);
			return;
		}
		
		BIO *out = BIO_pop(abio);

		int handshake = BIO_do_handshake(out);
		
		SSL *ssl;
		BIO_get_ssl(out, &ssl);
		int result = SSL_get_verify_result(ssl);
		if (result == X509_V_OK) {
			STAssertFalse(cert_fail, @"Client certificate passed verification");
		} else {
			STAssertTrue(cert_fail, @"Client certificate failed verification -- did you set it to trusted in your keychain (security add-certificate client.crt; security add-trusted-cert client.crt)?");
		}
		
		if (handshake <= 0)
		{
			STAssertTrue(handshake_fail, @"Handshake failed, resetting connection");
			BIO_free_all(out);
			return;
		} else {
			STAssertFalse(handshake_fail, @"Unexpected handshake success");
		}
		
		char msg[] = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: 34\r\n\r\n[+] Secure connection established\n";
		BIO_puts(out, msg);
		BIO_flush(out);
		
		BIO_free_all(out);
	};
	comm_test = Block_copy(comm_test);
	
	comm_teardown = ^{
        BIO_free_all(abio);
        SSL_CTX_free(ctx);
	};
	comm_teardown = Block_copy(comm_teardown);
};

+(void)tearDown
{
	if (comm_teardown) {
		comm_teardown();
	}
}

static void spawn_for_test(char *program, ...)
{
	const int m_args = 48;
	int a_count;
	char *args[m_args];
	va_list ap;
	
	va_start(ap, program);
	for(a_count = 0; a_count < m_args; a_count++) {
		args[a_count] = va_arg(ap, char*);
		if (args[a_count] == NULL) {
			break;
		}
	}
	if (a_count >= m_args) {
		errx(3, "Too many arguments (%d max; program='%s')", m_args, program);
	}
	va_end(ap);
	
	pid_t pid;
	int rc = posix_spawn(&pid, program, NULL, NULL, args, *_NSGetEnviron());
	if (rc != 0) {
		err(3, "spawn failed for program '%s' ", program);
	}
	
	// Avoid zombies, but we don't really care about processes exit code
	int ignored;
	waitpid(pid, &ignored, 0);
}

-(void)testTrustedKeyExists
{
	if (!comm_test) {
		comm_setup(self);
	}
	
	if (comm_test) {
		dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
			spawn_for_test("/usr/bin/curl", "curl", "-s", "-k", "-E", signed_pem, url, NULL);
		});
		comm_test(NO, YES);
	} else {
		STFail(@"Couldn't set up test");
	}
}

-(void)testTrustedKeyDoesntOveride
{
	if (!comm_test) {
		comm_setup(self);
	}

	if (comm_test) {
		dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
			spawn_for_test("/usr/bin/curl", "curl", "-s", "-k", "-E", client_pem, url, NULL);
		});
		comm_test(YES, YES);
	} else {
		STFail(@"Couldn't set up test");
	}
}

@end