From bb62ed3b1f3606ecf4310fe6451d7d060ac082f3 Mon Sep 17 00:00:00 2001 From: Jonathan Hodgson Date: Thu, 20 Jan 2022 16:53:35 +0000 Subject: [PATCH] Starts work on https as well as some moving about I now start the listener in the main.c file rather than proxy given that I didn't feel proxy was the right place if a normal (non-proxied) request came in. webserver.{c,h} and proxy.{c,h} had some changes relating to this. The config changed slightly - we now create a folder in ~/.config/ called yaip. This is where certificates and so on will be stored along with the user configuration I created a helper function to get files inside this directory (it changes based on xdg_config_home) and updated relevant tests. In ssl.{c,h} I have started work. If they don't exist, the tool now creates and stores a key and certificate for the CA that this tool will need to pretend to be. I still need to write tests for this. --- .gitignore | 1 + Makefile | 2 +- docs/certificate-authority.md | 6 ++ src/config.c | 37 ++++--- src/config.h | 5 +- src/main.c | 114 +++++++++++---------- src/proxy.c | 79 ++++++++------- src/proxy.h | 4 +- src/ssl.c | 180 ++++++++++++++++++++++++++++++++++ src/ssl.h | 19 ++++ src/webserver.c | 15 +++ src/webserver.h | 1 + tests/config.test.c | 12 ++- tests/request.test.c | 1 + 14 files changed, 371 insertions(+), 105 deletions(-) create mode 100644 docs/certificate-authority.md create mode 100644 src/ssl.c create mode 100644 src/ssl.h diff --git a/.gitignore b/.gitignore index 65c7bb9..c3e88ef 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ yaip mynotes.md *.sqlite +/certs diff --git a/Makefile b/Makefile index 020e605..44d3f33 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ TESTFILES = $(wildcard tests/*.c) TESTOUT = $(TESTFILES:.c=) OUT = yaip CFLAGS = -Wall -g -LDLIBS = -lsqlite3 -lm +LDLIBS = -lsqlite3 -lm -lssl -lcrypto CC = gcc diff --git a/docs/certificate-authority.md b/docs/certificate-authority.md new file mode 100644 index 0000000..3a328f3 --- /dev/null +++ b/docs/certificate-authority.md @@ -0,0 +1,6 @@ +# Certificate Authority + +At some point, it would be nice if yaip does this automatically, but for now, +you need to create a certificate authority for yaip to sign requests with. + +; diff --git a/src/config.c b/src/config.c index a1a8c79..2e4123a 100644 --- a/src/config.c +++ b/src/config.c @@ -34,35 +34,50 @@ char* resolveTilde(const char *path) { */ Config* configDefaults(){ Config *conf = malloc( sizeof( Config ) ); - conf->database = "proxy.sqlite"; + conf->database = strdup("proxy.sqlite"); conf->port = 8080; - conf->localConfig = "proxy.conf"; - conf->userConfig = getDefaultUserConfigLoc(); + conf->localConfig = strdup("proxy.conf"); + conf->userConfig = getUserConfigFile("proxy.conf"); + conf->certfile = getUserConfigFile("cert.pem"); + conf->keyfile = getUserConfigFile("key.pem"); return conf; } +void maybeMakeDir(const char *path){ + if ( path_exists( path ) ) return; + mkdir( path, 0700 ); +} + char* getConfigDir(){ char *xdg_config_home; if ((xdg_config_home = getenv("XDG_CONFIG_HOME")) == NULL) xdg_config_home = resolveTilde("~/.config"); + + char configDir[strlen(xdg_config_home) + 6]; + memset(configDir, '\0', strlen(xdg_config_home) + 6); - return xdg_config_home; + sprintf( configDir, "%s/yaip", xdg_config_home ); + + return strdup(configDir); } -char* getDefaultUserConfigLoc(){ +char* getUserConfigFile(char *filename){ + //This doesn't end in a slash char *configDir = getConfigDir(); - char configFile[strlen(configDir) + 11]; - - memset(configFile, '\0', strlen(configDir) + 11); + //Make sure our filename doesn't start with a slash + while ( filename[0] == '/' ) filename++; + // Allocate space for both parts, a slash and a \0 + char retFile[strlen(configDir) + strlen(filename) + 2]; - strcpy( configFile, configDir ); + memset(retFile, '\0', strlen(configDir) + strlen(filename) + 2); - strcat( configFile, "/proxy.conf" ); + sprintf(retFile, "%s/%s", configDir, filename); - return strdup(configFile); + return strdup(retFile); } + void setConfig(Config *config, char option[], char value[]){ if ( strcmp( option, "database" ) == 0 ){ config->database = value; diff --git a/src/config.h b/src/config.h index 738f617..8f9352f 100644 --- a/src/config.h +++ b/src/config.h @@ -13,13 +13,16 @@ typedef struct { char *localConfig; // Project Specific char *userConfig; // User Specific unsigned int port; + char *certfile; + char *keyfile; } Config; bool path_exists(const char *path); char* resolveTilde(const char *path); Config* configDefaults(); char* getConfigDir(); -char* getDefaultUserConfigLoc(); +void maybeMakeDir(const char *path); +char* getUserConfigFile(); void setConfig(Config *config, char option[], char value[]); void printConfig(Config *config); diff --git a/src/main.c b/src/main.c index b67abc5..16c68c7 100644 --- a/src/main.c +++ b/src/main.c @@ -7,6 +7,7 @@ #include "config.h" #include "request.h" #include "response.h" +#include "ssl.h" #define PACKAGE_NAME "WICTP" #define DEFAULT_DATABASE "data.sqlite" @@ -30,6 +31,40 @@ void printHelp(){ printf( "\t --print-config Prints current config\n" ); } +int startListener(unsigned int port){ + //we need to act as an http server + int server_fd; + struct sockaddr_in address; + memset( &address, 0, sizeof(address) ); + int addrlen = sizeof(address); + Response *response; + char *responseStr; + + // Creating socket file descriptor + if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) { + perror("socket failed"); + return -1; + } + + address.sin_family = AF_INET; + address.sin_addr.s_addr = INADDR_ANY; + address.sin_port = htons( port ); + + // Forcefully attaching socket to the port 8080 + if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) != 0) { + perror("bind failed"); + return -1; + } + + if (listen(server_fd, 3) != 0) { + perror("listen"); + return -1; + } + + return server_fd; +} + + int main(int argc, char**argv){ Config *config = configDefaults(); int listener; @@ -67,7 +102,24 @@ int main(int argc, char**argv){ db_create(config->database); } - listener = proxy_startListener(config->port); + maybeMakeDir( getConfigDir() ); + + + if (!path_exists( config->keyfile ) ){ + printf("Creating keyfile\n"); + if ( !create_and_save_key(config->keyfile) ){ + return 1; + } + } + + if (!path_exists( config->certfile ) ){ + printf("Creating cert\n"); + if ( !create_and_save_cert(config->certfile, read_private_key( config->keyfile) ) ){ + return 1; + } + } + + listener = startListener(config->port); if ( listener < 0 ){ return 1; @@ -78,8 +130,6 @@ int main(int argc, char**argv){ socklen_t addrlen = sizeof(addr); int client = 0; Request *request = NULL; - Response *response = NULL; - char *responseStr; printf("Listening on port %i\n", config->port); if ((client = accept(listener, (struct sockaddr *)&addr, @@ -93,62 +143,18 @@ int main(int argc, char**argv){ //thread request = newRequestFromSocket(client); - - // If the host is not defined, then it is not a proxy request - // Note that the host here is where the request should be sent, not - // necesarily the hosts header + if ( strcmp( request->host, "" ) == 0 ){ - response = webserverGetResponse(request); + // If the host is not defined, then it is not a proxy request + // Note that the host here is where the request should be sent, not + // necesarily the hosts header + webserverRequest(request, client); } else { - response = upstreamGetResponse(request); + proxyRequest(request, client); } - - responseStr = responseToString( response ); - - // I'm also not convinced that strlen is the best function to use here - // When we get to dealing with binary requests / responses, they may - // well have null characters in them - send(client , responseStr, strlen(responseStr) , 0 ); - - printf( "\n1\n" ); + close(client); - printf( "\n2\n" ); freeRequest( request ); - printf( "\n3\n" ); - freeResponse( response ); - printf( "\n4\n" ); - free(responseStr); - printf( "\n5\n" ); - - //If this is an https request - this is the first part - //if ( strcmp( request->method, "CONNECT" ) == 0 ){ - - // // I am basically doing the same thing that mitmproxy does here - // // We start by responding with 200 Connection Established which - // // in a normal proxy would mean that we have established a - // // connection with the remote host. However, we haven't because we - // // are going to pretend to be the host to the client and pretend to - // // be the client to the host - - // response = newResponse(); - // connectionEstablished(response); - // responseStr = responseToString(response); - // send(new_socket , responseStr, strlen(responseStr) , 0 ); - - - // char line[1024] = {'\0'}; - // //a length of 2 will indicate an empty line which will split the headers - // //from the body (if there is a body) - // int valread = read( new_socket, line, 1024); - // while (valread > 0){ - // printf("%s", line); - // //I believe at this point all the headers are done. - // valread = read( new_socket , line, 1024); - // } - - - - //} } diff --git a/src/proxy.c b/src/proxy.c index d76f77e..0bb5ff7 100644 --- a/src/proxy.c +++ b/src/proxy.c @@ -38,37 +38,50 @@ Response *upstreamGetResponse(Request *request){ } -int proxy_startListener(unsigned int port){ - //we need to act as an http server - int server_fd; - struct sockaddr_in address; - memset( &address, 0, sizeof(address) ); - int addrlen = sizeof(address); - Response *response; - char *responseStr; - - // Creating socket file descriptor - if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) { - perror("socket failed"); - return -1; - } - - address.sin_family = AF_INET; - address.sin_addr.s_addr = INADDR_ANY; - address.sin_port = htons( port ); - - // Forcefully attaching socket to the port 8080 - if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) != 0) { - perror("bind failed"); - return -1; - } - - if (listen(server_fd, 3) != 0) { - perror("listen"); - return -1; - } - - return server_fd; - - +void proxyRequest(Request *request, int client){ + + if ( strcmp( request->method, "CONNECT" ) == 0 ){ + // If it is a connect request, we are dealing with https + + // I am basically doing the same thing that mitmproxy does here + // We start by responding with 200 Connection Established which + // in a normal proxy would mean that we have established a + // connection with the remote host. However, we haven't because we + // are going to pretend to be the host to the client and pretend to + // be the client to the host + + Response *response = newResponse(); + connectionEstablished(response); + char *responseStr = responseToString(response); + send(client , responseStr, strlen(responseStr) , 0 ); + + + + + + //SSL_CTX *ctx; + //SSL *ssl; + //char buf[1024] = {0}; + //int bytes; + + //SSL_library_init(); + //ctx = InitServerCTX(config); + //ssl = SSL_new(ctx); + //SSL_set_fd( ssl, client ); + + //if ( SSL_accept(ssl) == -1 ){ + // ERR_print_errors_fp(stderr); + //} else { + // bytes = SSL_read(ssl, buf, sizeof(buf)); + // buf[bytes] = '\0'; + // printf("%s", buf); + //} + + } else { + Response *response = upstreamGetResponse(request); + char *responseStr = responseToString( response ); + send(client , responseStr, strlen(responseStr) , 0 ); + free( responseStr ); + freeResponse( response ); + } } diff --git a/src/proxy.h b/src/proxy.h index 2c5daf9..2eb5e88 100644 --- a/src/proxy.h +++ b/src/proxy.h @@ -12,9 +12,11 @@ #include #include "request.h" +#include "response.h" #include "webserver.h" -int proxy_startListener( unsigned int port ); Response *upstreamGetResponse(Request *request); +void proxyRequest(Request *request, int client); + #endif /* ifndef PROXY_H */ diff --git a/src/ssl.c b/src/ssl.c new file mode 100644 index 0000000..b0f71ad --- /dev/null +++ b/src/ssl.c @@ -0,0 +1,180 @@ +#include "ssl.h" + + +SSL_CTX* InitServerCTX(Config *config) { + const SSL_METHOD *method; + SSL_CTX *ctx; + OpenSSL_add_all_algorithms(); /* load & register all cryptos, etc. */ + SSL_load_error_strings(); /* load all error messages */ + method = TLS_server_method(); /* create new server-method instance */ + ctx = SSL_CTX_new(method); /* create new context from method */ + if ( ctx == NULL ) { + ERR_print_errors_fp(stderr); + abort(); + } + + //set the local certificate from CertFile + if ( SSL_CTX_use_certificate_file(ctx, config->certfile, SSL_FILETYPE_PEM) <= 0 ){ + ERR_print_errors_fp(stderr); + abort(); + } + + //set the private key from KeyFile (may be the same as CertFile) + if ( SSL_CTX_use_PrivateKey_file(ctx, config->keyfile, SSL_FILETYPE_PEM) <= 0 ){ + ERR_print_errors_fp(stderr); + abort(); + } + //verify private key + if ( !SSL_CTX_check_private_key(ctx) ) + { + fprintf(stderr, "Private key does not match the public certificate\n"); + abort(); + } + + return ctx; +} + + +// Generates a 2048-bit RSA key. +// Largely stolen from here: https://gist.github.com/nathan-osman/5041136 +EVP_PKEY* generate_ca_key() { + // Allocate memory for the EVP_PKEY structure. + EVP_PKEY *pkey = EVP_PKEY_new(); + if(!pkey) { + perror("Unable to create EVP_PKEY structure.\n"); + return NULL; + } + + // Generate the RSA key and assign it to pkey. + RSA *rsa = RSA_new(); + BIGNUM *bn = BN_new(); + BN_set_word(bn, RSA_F4); + RSA_generate_key_ex(rsa, 2048, bn, NULL ); + if(!EVP_PKEY_assign_RSA(pkey, rsa)) { + perror("Unable to generate 2048-bit RSA key.\n"); + EVP_PKEY_free(pkey); + return NULL; + } + + // The key has been generated, return it. + return pkey; +} + + +// Generates a self-signed x509 certificate. +// Largely stolen from here: https://gist.github.com/nathan-osman/5041136 +X509* generate_ca_cert(EVP_PKEY * pkey) { + // Allocate memory for the X509 structure. + X509 *x509 = X509_new(); + if(!x509) { + perror( "Unable to create X509 structure.\n"); + return NULL; + } + + // Set the serial number. + ASN1_INTEGER_set(X509_get_serialNumber(x509), 1); + + // This certificate is valid from now until exactly one year from now. + X509_gmtime_adj(X509_get_notBefore(x509), 0); + X509_gmtime_adj(X509_get_notAfter(x509), 31536000L); + + // Set the public key for our certificate. + X509_set_pubkey(x509, pkey); + + // We want to copy the subject name to the issuer name. + X509_NAME *name = X509_get_subject_name(x509); + + // Set the country code and common name. + X509_NAME_add_entry_by_txt(name, "C", MBSTRING_ASC, (unsigned char *)"UK", -1, -1, 0); + X509_NAME_add_entry_by_txt(name, "O", MBSTRING_ASC, (unsigned char *)"Yet Another Intercepting Proxy", -1, -1, 0); + X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC, (unsigned char *)"jn.hn", -1, -1, 0); + + // Now set the issuer name. + X509_set_issuer_name(x509, name); + + + // TODO: mitmproxy adds some extensions. I don' tknow if I need those + // https://github.com/mitmproxy/mitmproxy/blob/3cb89069b9f70c198cc89337bd5a9b1bbf3a69d0/mitmproxy/certs.py#L190-L204 + + + // Actually sign the certificate with our key. + if(!X509_sign(x509, pkey, EVP_sha1())) + { + perror( "Error signing certificate.\n"); + X509_free(x509); + return NULL; + } + + + + return x509; +} + +bool create_and_save_key(char keyfile[]) { + // Generate the key + EVP_PKEY *pkey = generate_ca_key(); + if ( pkey == NULL ){ + fprintf(stderr, "Unable to open generate key\n"); + return false; + } + + // Open the PEM file for writing the key to disk. + FILE * pkey_file = fopen(keyfile, "wb"); + if(!pkey_file) { + fprintf(stderr, "Unable to open \"%s\" for writing.\n", keyfile); + return false; + } + // Write the key to disk. + bool ret = PEM_write_PrivateKey(pkey_file, pkey, NULL, NULL, 0, NULL, NULL); + fclose(pkey_file); + + if(!ret) { + perror("Unable to write private key to disk.\n"); + return false; + } + return true; +} + +EVP_PKEY* read_private_key(char keyfile[]){ + + FILE *fp; + EVP_PKEY *pkey; + + if (! (fp = fopen(keyfile, "r"))){ + perror("Error cant open certificate private key file.\n"); + return NULL; + } + + pkey = PEM_read_PrivateKey( fp, NULL, NULL, NULL ); + + if ( pkey == NULL ){ + perror("Error cant read certificate private key file.\n"); + } + + return pkey; +} + +bool create_and_save_cert(char certfile[], EVP_PKEY *pkey) { + // Open the PEM file for writing the certificate to disk. + FILE * x509_file = fopen(certfile, "wb"); + if(!x509_file) { + fprintf(stderr, "Unable to open \"%s\" for writing.\n", certfile); + return false; + } + + X509 *x509 = generate_ca_cert(pkey); + if ( x509 == NULL ){ + fprintf(stderr, "Unable to open generate cert\n"); + return false; + } + + // Write the certificate to disk. + int ret = PEM_write_X509(x509_file, x509); + fclose(x509_file); + + if(!ret) { + perror("Unable to write certificate to disk.\n"); + return false; + } + return true; +} diff --git a/src/ssl.h b/src/ssl.h new file mode 100644 index 0000000..aca8e0f --- /dev/null +++ b/src/ssl.h @@ -0,0 +1,19 @@ +#ifndef SSL_H +#define SSL_H + +#include +#include +#include +#include + +#include "config.h" + +SSL_CTX* InitServerCTX(Config *config); + +EVP_PKEY* generate_ca_key(); +X509* generate_ca_cert(EVP_PKEY * pkey); +bool create_and_save_key(char keyfile[]); +bool create_and_save_cert(char keyfile[], EVP_PKEY *pkey); +EVP_PKEY* read_private_key(char keyfile[]); + +#endif /* ifndef SSL_ */ diff --git a/src/webserver.c b/src/webserver.c index bc18835..aa740ee 100644 --- a/src/webserver.c +++ b/src/webserver.c @@ -6,3 +6,18 @@ Response* webserverGetResponse( Request *req ){ responseSetBody(rsp, "Test", 1); return rsp; } + +void webserverRequest( Request *req, int client ){ + Response *rsp = webserverGetResponse( req ); + char *responseStr = responseToString( rsp ); + + // I'm also not convinced that strlen is the best function to use here + // When we get to dealing with binary requests / responses, they may + // well have null characters in them + send(client , responseStr, strlen(responseStr) , 0 ); + + free( responseStr ); + freeResponse( rsp ); + +} + diff --git a/src/webserver.h b/src/webserver.h index d4867e3..f006a39 100644 --- a/src/webserver.h +++ b/src/webserver.h @@ -8,6 +8,7 @@ #include "response.h" Response* webserverGetResponse( Request *req ); +void webserverRequest( Request *req, int client); #endif /* ifndef WEBSERVER_H diff --git a/tests/config.test.c b/tests/config.test.c index 799c516..f35ce39 100644 --- a/tests/config.test.c +++ b/tests/config.test.c @@ -37,7 +37,7 @@ MunitResult checkConfigDirWithXdg(const MunitParameter params[], void* user_data_or_fixture){ char directory[] = "/testing/xdg/directory"; setenv( "XDG_CONFIG_HOME",directory, 1 ); - munit_assert_string_equal( directory, getConfigDir() ); + munit_assert_string_equal( "/testing/xdg/directory/yaip", getConfigDir() ); return MUNIT_OK; } @@ -46,7 +46,7 @@ MunitResult checkConfigDirWithoutXdg(const MunitParameter params[], unsetenv( "XDG_CONFIG_HOME" ); char dir[500] = {'\0'}; strcpy( dir, getenv("HOME") ); - strcat( dir, "/.config" ); + strcat( dir, "/.config/yaip" ); munit_assert_string_equal( dir, getConfigDir() ); return MUNIT_OK; } @@ -55,14 +55,18 @@ MunitResult checkDefaults(const MunitParameter params[], void* user_data_or_fixture){ char directory[] = "/testing/xdg/directory"; - char file[] = "/testing/xdg/directory/proxy.conf"; + char conffile[] = "/testing/xdg/directory/yaip/proxy.conf"; + char certfile[] = "/testing/xdg/directory/yaip/cert.pem"; + char keyfile[] = "/testing/xdg/directory/yaip/key.pem"; setenv( "XDG_CONFIG_HOME",directory, 1 ); Config *conf = configDefaults(); munit_assert_string_equal(conf->database, "proxy.sqlite"); munit_assert_int(conf->port, ==, 8080); munit_assert_string_equal(conf->localConfig, "proxy.conf"); - munit_assert_string_equal(conf->userConfig, file); + munit_assert_string_equal(conf->userConfig, conffile); + munit_assert_string_equal(conf->certfile, certfile); + munit_assert_string_equal(conf->keyfile, keyfile); return MUNIT_OK; } diff --git a/tests/request.test.c b/tests/request.test.c index 168a940..6bb895c 100644 --- a/tests/request.test.c +++ b/tests/request.test.c @@ -114,6 +114,7 @@ MunitResult testFirstLineVersions(const MunitParameter params[], void* user_data_or_fixture){ Request *req; + printf("\nI get here\n"); requestTestFirstLine *line = getLineObj(params); if ( line->fullLine == NULL ) return MUNIT_ERROR; req = newRequest();