-
-
Save darrenjs/4645f115d10aa4b5cebf57483ec82eca to your computer and use it in GitHub Desktop.
| /* | |
| This file had now been added to the git repo | |
| https://github.com/darrenjs/openssl_examples | |
| ... which also includes a non blocking client example. | |
| ------------------------------------------------------------------------------- | |
| ssl_server_nonblock.c -- Copyright 2017 Darren Smith -- MIT license | |
| Permission is hereby granted, free of charge, to any person obtaining a copy of | |
| this software and associated documentation files (the "Software"), to deal in | |
| the Software without restriction, including without limitation the rights to | |
| use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of | |
| the Software, and to permit persons to whom the Software is furnished to do so, | |
| subject to the following conditions: | |
| The above copyright notice and this permission notice shall be included in all | |
| copies or substantial portions of the Software. | |
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS | |
| FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR | |
| COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER | |
| IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN | |
| CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | |
| ------------------------------------------------------------------------------- | |
| ssl_server_nonblock.c is a simple OpenSSL example program to illustrate the use | |
| of memory BIO's (BIO_s_mem) to perform SSL read and write with non-blocking | |
| socket IO. | |
| The program accepts connections from SSL clients. To keep it simple only a | |
| single live connection is supported. While a client is connected the program | |
| will receive any bytes which it sends, unencrypt them and write to stdout, using | |
| non-blocking socket reads. It will also read from stdin, encrypt the bytes and | |
| send to the client, using non-blocking socket writes. | |
| Note that this program is single threaded. This means it does not have to set up | |
| SSL locking. The program does not exit, and so it does not have code to free up | |
| the resources associated with the SSL context and library. | |
| Compilation | |
| ----------- | |
| To compile the program, use something like: | |
| gcc -Wall -O0 -g3 -std=c99 ssl_server_nonblock.c -o ssl_server_nonblock -lcrypto -lssl | |
| Running | |
| ------- | |
| Running the program requires that a SSL certificate and private key are | |
| available to be loaded. These can be generated using the 'openssl' program using | |
| these steps: | |
| 1. Generate the private key, this is what we normally keep secret: | |
| openssl genrsa -des3 -passout pass:xxxx -out server.pass.key 2048 | |
| openssl rsa -passin pass:xxxx -in server.pass.key -out server.key | |
| rm -f server.pass.key | |
| 2. Next generate the CSR. We can leave the password empty when prompted | |
| (because this is self-sign): | |
| openssl req -new -key server.key -out server.csr | |
| 3. Next generate the self signed certificate: | |
| openssl x509 -req -sha256 -days 365 -in server.csr -signkey server.key -out server.crt | |
| rm -f server.csr | |
| The openssl program can also be used to connect to this program as an SSL | |
| client. Here's an example command (assuming we're using port 55555): | |
| openssl s_client -connect 127.0.0.1:55555 -msg -debug -state -showcerts | |
| Flow of encrypted & unencrypted bytes | |
| ------------------------------------- | |
| This diagram shows how the read and write memory BIO's (rbio & wbio) are | |
| associated with the socket read and write respectively. On the inbound flow | |
| (data into the program) bytes are read from the socket and copied into the rbio | |
| via BIO_write. This represents the the transfer of encrypted data into the SSL | |
| object. The unencrypted data is then obtained through calling SSL_read. The | |
| reverse happens on the outbound flow to convey unencrypted user data into a | |
| socket write of encrypted data. | |
| +------+ +-----+ | |
| |......|--> read(fd) --> BIO_write(rbio) -->|.....|--> SSL_read(ssl) --> IN | |
| |......| |.....| | |
| |.sock.| |.SSL.| | |
| |......| |.....| | |
| |......|<-- write(fd) <-- BIO_read(wbio) <--|.....|<-- SSL_write(ssl) <-- OUT | |
| +------+ +-----+ | |
| | | | | | |
| |<-------------------------------->| |<------------------->| | |
| | encrypted bytes | | unencrypted bytes | | |
| */ | |
| #include <openssl/bio.h> | |
| #include <openssl/err.h> | |
| #include <openssl/pem.h> | |
| #include <openssl/ssl.h> | |
| #include <arpa/inet.h> | |
| #include <poll.h> | |
| #include <stdio.h> | |
| #include <string.h> | |
| #include <sys/socket.h> | |
| #include <sys/types.h> | |
| #include <unistd.h> | |
| #define DEFAULT_BUF_SIZE 64 | |
| void handle_error(const char *file, int lineno, const char *msg) { | |
| fprintf(stderr, "** %s:%i %s\n", file, lineno, msg); | |
| ERR_print_errors_fp(stderr); | |
| exit(1); | |
| } | |
| #define int_error(msg) handle_error(__FILE__, __LINE__, msg) | |
| void die(const char *msg) { | |
| perror(msg); | |
| exit(1); | |
| } | |
| void print_unencrypted_data(char *buf, size_t len) { | |
| printf("%.*s", (int)len, buf); | |
| } | |
| /* Global SSL context */ | |
| SSL_CTX *ctx; | |
| /* This structure manages the data and functions associated with a SSL/TLS | |
| socket. A single instance of this struct is used through out the example | |
| because the program only allow a single connection at any time. The struct | |
| stores the file descriptor, the SSL objects, and data which is waiting to | |
| be either written to socket or encrypted. */ | |
| struct ssl_client | |
| { | |
| int fd; | |
| SSL *ssl; | |
| BIO *rbio; /* SSL reads from, we write to. */ | |
| BIO *wbio; /* SSL writes to, we read from. */ | |
| /* Bytes waiting to be written to socket. This is data that has been generated | |
| * by the SSL object, either due to encryption of user input, or, writes | |
| * requires due to peer-requested SSL renegotiation. */ | |
| char* write_buf; | |
| size_t write_len; | |
| /* Bytes waiting to be fed into the SSL object for encryption. */ | |
| char* encrypt_buf; | |
| size_t encrypt_len; | |
| /* Method to invoke when unencrypted bytes are available. */ | |
| void (*io_on_read)(char *buf, size_t len); | |
| } client; | |
| void ssl_client_init(struct ssl_client *p) | |
| { | |
| memset(p, 0, sizeof(struct ssl_client)); | |
| p->rbio = BIO_new(BIO_s_mem()); | |
| p->wbio = BIO_new(BIO_s_mem()); | |
| p->ssl = SSL_new(ctx); | |
| SSL_set_accept_state(p->ssl); /* sets ssl to work in server mode. */ | |
| SSL_set_bio(p->ssl, p->rbio, p->wbio); | |
| p->io_on_read = print_unencrypted_data; | |
| } | |
| void ssl_client_cleanup(struct ssl_client *p) | |
| { | |
| SSL_free(p->ssl); /* free the SSL object and its BIO's */ | |
| free(p->write_buf); | |
| free(p->encrypt_buf); | |
| } | |
| int ssl_client_want_write(struct ssl_client *cp) { | |
| return (cp->write_len>0); | |
| } | |
| /* Obtain the return value of an SSL operation and convert into a simplified | |
| * error code, which is easier to examine for failure. */ | |
| enum sslstatus { SSLSTATUS_OK, SSLSTATUS_WANT_IO, SSLSTATUS_FAIL}; | |
| static enum sslstatus get_sslstatus(SSL* ssl, int n) | |
| { | |
| switch (SSL_get_error(ssl, n)) | |
| { | |
| case SSL_ERROR_NONE: | |
| return SSLSTATUS_OK; | |
| case SSL_ERROR_WANT_WRITE: | |
| case SSL_ERROR_WANT_READ: | |
| return SSLSTATUS_WANT_IO; | |
| case SSL_ERROR_ZERO_RETURN: | |
| case SSL_ERROR_SYSCALL: | |
| default: | |
| return SSLSTATUS_FAIL; | |
| } | |
| } | |
| /* Handle request to send unencrypted data to the SSL. All we do here is just | |
| * queue the data into the encrypt_buf for later processing by the SSL | |
| * object. */ | |
| void send_unencrypted_bytes(const char *buf, size_t len) | |
| { | |
| client.encrypt_buf = (char*)realloc(client.encrypt_buf, client.encrypt_len + len); | |
| memcpy(client.encrypt_buf+client.encrypt_len, buf, len); | |
| client.encrypt_len += len; | |
| } | |
| /* Queue encrypted bytes for socket write. Should only be used when the SSL | |
| * object has requested a write operation. */ | |
| void queue_encrypted_bytes(const char *buf, size_t len) | |
| { | |
| client.write_buf = (char*)realloc(client.write_buf, client.write_len + len); | |
| memcpy(client.write_buf+client.write_len, buf, len); | |
| client.write_len += len; | |
| } | |
| /* Process SSL bytes received from the peer. The data needs to be fed into the | |
| SSL object to be unencrypted. On success returns 0, on SSL error -1. */ | |
| int on_read_cb(char* src, size_t len) | |
| { | |
| char buf[DEFAULT_BUF_SIZE]; /* used for copying bytes out of SSL/BIO */ | |
| enum sslstatus status; | |
| int n; | |
| while (len > 0) { | |
| n = BIO_write(client.rbio, src, len); | |
| if (n<=0) | |
| return -1; /* if BIO write fails, assume unrecoverable */ | |
| src += n; | |
| len -= n; | |
| if (!SSL_is_init_finished(client.ssl)) { | |
| n = SSL_accept(client.ssl); | |
| status = get_sslstatus(client.ssl, n); | |
| /* Did SSL request to write bytes? */ | |
| if (status == SSLSTATUS_WANT_IO) | |
| do { | |
| n = BIO_read(client.wbio, buf, sizeof(buf)); | |
| if (n > 0) | |
| queue_encrypted_bytes(buf, n); | |
| else if (!BIO_should_retry(client.wbio)) | |
| return -1; | |
| } while (n>0); | |
| if (status == SSLSTATUS_FAIL) | |
| return -1; | |
| if (!SSL_is_init_finished(client.ssl)) | |
| return 0; | |
| } | |
| /* The encrypted data is now in the input bio so now we can perform actual | |
| * read of unencrypted data. */ | |
| do { | |
| n = SSL_read(client.ssl, buf, sizeof(buf)); | |
| if (n > 0) | |
| client.io_on_read(buf, (size_t)n); | |
| } while (n > 0); | |
| status = get_sslstatus(client.ssl, n); | |
| /* Did SSL request to write bytes? This can happen if peer has requested SSL | |
| * renegotiation. */ | |
| if (status == SSLSTATUS_WANT_IO) | |
| do { | |
| n = BIO_read(client.wbio, buf, sizeof(buf)); | |
| if (n > 0) | |
| queue_encrypted_bytes(buf, n); | |
| else if (!BIO_should_retry(client.wbio)) | |
| return -1; | |
| } while (n>0); | |
| if (status == SSLSTATUS_FAIL) | |
| return -1; | |
| } | |
| return 0; | |
| } | |
| /* Process outbound unencrypted data that are waiting to be encrypted. The | |
| * waiting data resides in encrypt_buf. It needs to be passed into the SSL | |
| * object for encryption, which in turn generates the encrypted bytes that then | |
| * will be queued for later socket write. */ | |
| int do_encrypt() | |
| { | |
| char buf[DEFAULT_BUF_SIZE]; | |
| enum sslstatus status; | |
| if (!SSL_is_init_finished(client.ssl)) | |
| return 0; | |
| while (client.encrypt_len>0) { | |
| int n = SSL_write(client.ssl, client.encrypt_buf, client.encrypt_len); | |
| status = get_sslstatus(client.ssl, n); | |
| if (n>0) { | |
| /* consume the waiting bytes that have been used by SSL */ | |
| if ((size_t)n<client.encrypt_len) | |
| memmove(client.encrypt_buf, client.encrypt_buf+n, client.encrypt_len-n); | |
| client.encrypt_len -= n; | |
| client.encrypt_buf = (char*)realloc(client.encrypt_buf, client.encrypt_len); | |
| /* take the output of the SSL object and queue it for socket write */ | |
| do { | |
| n = BIO_read(client.wbio, buf, sizeof(buf)); | |
| if (n > 0) | |
| queue_encrypted_bytes(buf, n); | |
| else if (!BIO_should_retry(client.wbio)) | |
| return -1; | |
| } while (n>0); | |
| } | |
| if (status == SSLSTATUS_FAIL) | |
| return -1; | |
| if (n==0) | |
| break; | |
| } | |
| return 0; | |
| } | |
| /* Read bytes from stdin and queue for later encryption. */ | |
| void do_stdin_read() | |
| { | |
| char buf[DEFAULT_BUF_SIZE]; | |
| ssize_t n = read(STDIN_FILENO, buf, sizeof(buf)); | |
| if (n>0) | |
| send_unencrypted_bytes(buf, (size_t)n); | |
| } | |
| /* Read encrypted bytes from socket. */ | |
| int do_sock_read() | |
| { | |
| char buf[DEFAULT_BUF_SIZE]; | |
| ssize_t n = read(client.fd, buf, sizeof(buf)); | |
| if (n>0) | |
| return on_read_cb(buf, (size_t)n); | |
| else | |
| return -1; | |
| } | |
| /* Write encrypted bytes to the socket. */ | |
| int do_sock_write() | |
| { | |
| ssize_t n = write(client.fd, client.write_buf, client.write_len); | |
| if (n>0) { | |
| if ((size_t)n<client.write_len) | |
| memmove(client.write_buf, client.write_buf+n, client.write_len-n); | |
| client.write_len -= n; | |
| client.write_buf = (char*)realloc(client.write_buf, client.write_len); | |
| return 0; | |
| } | |
| else | |
| return -1; | |
| } | |
| void ssl_init() { | |
| printf("initialising SSL\n"); | |
| /* SSL library initialisation */ | |
| SSL_library_init(); | |
| OpenSSL_add_all_algorithms(); | |
| SSL_load_error_strings(); | |
| ERR_load_BIO_strings(); | |
| ERR_load_crypto_strings(); | |
| /* create the SSL server context */ | |
| ctx = SSL_CTX_new(SSLv23_server_method()); | |
| if (!ctx) | |
| die("SSL_CTX_new()"); | |
| /* Load certificate and private key files, and check consistency */ | |
| int err; | |
| err = SSL_CTX_use_certificate_file(ctx, "server.crt", SSL_FILETYPE_PEM); | |
| if (err != 1) | |
| int_error("SSL_CTX_use_certificate_file failed"); | |
| else | |
| printf("certificate file loaded ok\n"); | |
| /* Indicate the key file to be used */ | |
| err = SSL_CTX_use_PrivateKey_file(ctx, "server.key", SSL_FILETYPE_PEM); | |
| if (err != 1) | |
| int_error("SSL_CTX_use_PrivateKey_file failed"); | |
| else | |
| printf("private-key file loaded ok\n"); | |
| /* Make sure the key and certificate file match. */ | |
| if (SSL_CTX_check_private_key(ctx) != 1) | |
| int_error("SSL_CTX_check_private_key failed"); | |
| else | |
| printf("private key verified ok\n"); | |
| /* Recommended to avoid SSLv2 & SSLv3 */ | |
| SSL_CTX_set_options(ctx, SSL_OP_ALL|SSL_OP_NO_SSLv2|SSL_OP_NO_SSLv3); | |
| } | |
| int main(int argc, char **argv) | |
| { | |
| char str[INET_ADDRSTRLEN]; | |
| int port = (argc>1)? atoi(argv[1]):55555; | |
| int servfd = socket(AF_INET, SOCK_STREAM, 0); | |
| if (servfd < 0) | |
| die("socket()"); | |
| int enable = 1; | |
| if (setsockopt(servfd, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(enable)) < 0) | |
| die("setsockopt(SO_REUSEADDR)"); | |
| /* Specify socket address */ | |
| struct sockaddr_in servaddr; | |
| memset(&servaddr, 0, sizeof(servaddr)); | |
| servaddr.sin_family = AF_INET; | |
| servaddr.sin_addr.s_addr = htonl(INADDR_ANY); | |
| servaddr.sin_port = htons(port); | |
| if (bind(servfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) | |
| die("bind()"); | |
| if (listen(servfd, 128) < 0) | |
| die("listen()"); | |
| int clientfd; | |
| struct sockaddr_in peeraddr; | |
| socklen_t peeraddr_len = sizeof(peeraddr); | |
| struct pollfd fdset[2]; | |
| memset(&fdset, 0, sizeof(fdset)); | |
| fdset[0].fd = STDIN_FILENO; | |
| fdset[0].events = POLLIN; | |
| ssl_init(); | |
| while (1) { | |
| printf("waiting for next connection on port %d\n", port); | |
| clientfd = accept(servfd, (struct sockaddr *)&peeraddr, &peeraddr_len); | |
| if (clientfd < 0) | |
| die("accept()"); | |
| ssl_client_init(&client); | |
| client.fd = clientfd; | |
| inet_ntop(peeraddr.sin_family, &peeraddr.sin_addr, str, INET_ADDRSTRLEN); | |
| printf("new connection from %s:%d\n", str, ntohs(peeraddr.sin_port)); | |
| fdset[1].fd = clientfd; | |
| /* event loop */ | |
| fdset[1].events = POLLERR | POLLHUP | POLLNVAL | POLLIN; | |
| #ifdef POLLRDHUP | |
| fdset[1].events |= POLLRDHUP; | |
| #endif | |
| while (1) { | |
| fdset[1].events &= ~POLLOUT; | |
| fdset[1].events |= (ssl_client_want_write(&client)? POLLOUT : 0); | |
| int nready = poll(&fdset[0], 2, -1); | |
| if (nready == 0) | |
| continue; /* no fd ready */ | |
| int revents = fdset[1].revents; | |
| if (revents & POLLIN) | |
| if (do_sock_read() == -1) | |
| break; | |
| if (revents & POLLOUT) | |
| if (do_sock_write() == -1) | |
| break; | |
| if (revents & (POLLERR | POLLHUP | POLLNVAL)) | |
| break; | |
| #ifdef POLLRDHUP | |
| if (revents & POLLRDHUP) | |
| break; | |
| #endif | |
| if (fdset[0].revents & POLLIN) | |
| do_stdin_read(); | |
| if (client.encrypt_len>0) | |
| do_encrypt(); | |
| } | |
| close(fdset[1].fd); | |
| ssl_client_cleanup(&client); | |
| } | |
| return 0; | |
| } |
the handshake is initiated in the client (rather than in the server, which is shown above), see the client example at https://github.com/darrenjs/openssl_examples
thanks very much, this example really helps.
Inspired by your gist! It helped shape my project "coroio" (C++20 coroutines & SSL) (https://github.com/resetius/coroio/blob/master/coroio/ssl.hpp)
Thanks for the code. Always good to have running code to start with.
Firstly a couple of admin nits:
-
compile should be the following to avoid ld errors (at least on my system)
gcc -Wall -O0 -g3 -std=c99 -o ssl_server_nonblock ssl_server_nonblock.c -lssl -l crypto -
newer versions of openssl enforce a minimum 4 char password
openssl genrsa -des3 -passout pass:xxxx -out server.pass.key 2048
openssl rsa -passin pass:xxxx -in server.pass.key -out server.key
openssl req -new -key server.key -out server.csr
openssl x509 -req -sha256 -days 365 -in server.csr -signkey server.key -out server.crt
3: The comments say
/* An instance of this object is created each time a client connection is
- accepted. It stores the client file descriptor, the SSL objects, and data
- which is waiting to be either written to socket or encrypted. */
struct ssl_client
...
} client;
But all I see is a global variable "client"
ssl_client_init(&client);
Often this does not even seem to be passed as a reference to functions.
Question: Is there a reason for that?
Question: Would it be beneficial if I looked at changing this to use malloc and pass pointers everywhere? So that there really is one set of data per client.
I ask because I'm looking at extending this to be multi-threaded. I understand it's OK to share an SSL context, but other data maybe not (like file descriptor). https://stackoverflow.com/questions/29195960/context-for-multiple-connections-with-openssl
For others who arrived here via a search engine, a more complete/modern form is available here; https://github.com/darrenjs/openssl_examples
@v6ops thanks, applied your suggestions.
thanks for making this example and repository. i really struggled to understand the concept, and now i do.
one question, handshake fails after client hello. i used example code from repository. do you have any idea why this is happening?
thanks in advance
Where is the code to do the handshake: SSL_do_handshake?