Skip to content

Instantly share code, notes, and snippets.

@dsoprea
Forked from darrenjs/ssl_server_nonblock.c
Created January 24, 2019 04:38
Show Gist options
  • Save dsoprea/518ea6d919d085358887d6ba51fce76b to your computer and use it in GitHub Desktop.
Save dsoprea/518ea6d919d085358887d6ba51fce76b to your computer and use it in GitHub Desktop.
OpenSSL example using memory BIO with non-blocking socket IO
/*
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 -lcrypto -lssl -o ssl_server_nonblock ssl_server_nonblock.c
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:x -out server.pass.key 2048
openssl rsa -passin pass:x -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;
/* 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
{
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;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment