Notes on Some Basic Uses of OpenSSL
OpenSSL is a widely used toolkit including a cryptography and a TLS library. In addition, it also includes a command line utility providing access the cryptography algorithms it implements. In practice, OpenSSL is one of the most widely used cryptography libraries. In fact, if you downloaded this document from iCorsi, most probably your browser uses OpenSSL to open a HTTPS connection with the iCorsi server to download the file. This document aims to give a brief introduction to some of the OpenSSL functionalities. In particular, it will cover the API offered by OpenSSL version 3. One of the major differences with previous versions is the deprecation of the low-level APIs in favor of high-level ones. In each section of this document, we will present a common use case for OpenSSL. We will first present how the CLI utility can be used to solve the tasks. Then, we will present how the same task can be done programmatically using the C API.
Symmetric Encryption/Decryption
This section gives an overview about how to encrypt some data using a block cipher. For our examples, we will use AES-256 in Cipher Block Chaining (CBC) mode. Let’s start by generating the plaintext we will use in our demo using the following command:
$ echo "Some plaintext to encrypt" > plaintext.txt
Before doing any encryption with a block cipher, we need to generate a
key and an Initialization Vector (IV). OpenSSL can be used as a source
of random bytes via the rand sub-command. AES-256 has a block size of
16 bytes, so we can use OpenSSL to generate a 16 bytes IV as follows:
$ openssl rand -hex 16 f7df0707d564a68d2021b0bee264fd32
Then, AES-256 also requires a 32 bytes long key which can be generated with OpenSSL as follows:
$ openssl rand -hex 32 23ad8d7ade5f23edc8e50504a9bc0bd774e3967a953cd29581ef791129652304
Finally, we can encrypt our message using the enc sub-command. The
enc sub-command allows to specify the cipher via options, For
AES-256 in CBC mode, the option is -aes-256-cbc. Hence, the full
command to encrypt our message is the following:
$ openssl enc \ -aes-256-cbc \ -K 23ad8d7ade5f23edc8e50504a9bc0bd774e3967a953cd29581ef791129652304 \ -iv f7df0707d564a68d2021b0bee264fd32 \ -e -in plaintext.txt \ -out ciphertext.txt
The above command will create a file named ciphertext.txt containing
the message encrypted using the key provided via the -K option and
the IV provided by the -iv option. Then, we can
decrypt the ciphertext and recover the original message using the
following command:
$ openssl enc \ -aes-256-cbc \ -K 23ad8d7ade5f23edc8e50504a9bc0bd774e3967a953cd29581ef791129652304 \ -iv f7df0707d564a68d2021b0bee264fd32 \ -d -in ciphertext.txt Some plaintext to encrypt
Note that we still use the enc sub-command, but are specifying the
-d option. In general, the -e is for encryption, while the -d
option is for decryption.
As stated in the introduction, OpenSSL offers a C API. It means that the same problem can also be solved in a C program. The following program shows how to encrypt/decrypt a message with AES-256 in a C program:
// Include this header file to use the RAND_bytes // function. #include <openssl/rand.h> // Include this header file to use the encryption/decryption API. #include <openssl/evp.h> // Include this header file to do error handling. #include <openssl/err.h> #include <stdio.h> #include <stdlib.h> int main(void) { unsigned char key[32]; unsigned char iv[16]; int len; int ciphertext_len; int decrypted_len; unsigned char decrypted_text[32]; // The example plaintext we want to encrypt. unsigned char plaintext[] = "Some plaintext to encrypt"; // A buffer that will contain the resulting ciphertext. This // buffer should be large enough to contain all the // ciphertext. The ciphertext will be larger than the plaintext // since it should include the padding that makes the plaintext a // multiple of the block size. unsigned char ciphertext[32]; // Generate 16 random bytes of Initialization Vector. RAND_bytes(iv, sizeof(iv)); // Generate 32 random bytes of key (AES-256 requires a 32 bytes // key) RAND_bytes(key, sizeof(key)); // Initialize an encryption context. The context will contain // all the configurations of the encryptions process, such as // the cipher used, the key, etc. EVP_CIPHER_CTX *enc_ctx = EVP_CIPHER_CTX_new(); if (!enc_ctx) { ERR_print_errors_fp(stderr); return EXIT_FAILURE; } // Configure the encryption context by providing the cipher, the // key and the Initialization Vector. if (EVP_EncryptInit(enc_ctx, EVP_aes_256_cbc(), key, iv) != 1) { EVP_CIPHER_CTX_free(enc_ctx); ERR_print_errors_fp(stderr); return EXIT_FAILURE; } // Encrypt the plaintext, and write the result in the ciphertext // buffer. Note that this function can be called multiple times. if(EVP_EncryptUpdate(enc_ctx, ciphertext, &len, plaintext, sizeof(plaintext) - 1) != 1) { EVP_CIPHER_CTX_free(enc_ctx); ERR_print_errors_fp(stderr); return EXIT_FAILURE; } ciphertext_len = len; // Finalize the encryption process. This function will flush all // the remaining blocks into the ciphertext buffer. Since you can // call EVP_EncryptUpdate multiple times, OpenSSL is not sure // about the length of the entire plaintext to perform padding. // Hence, call this function to inform it that you have finished // providing all the plaintext, so OpenSSL can finalize the // encryption. if(EVP_EncryptFinal(enc_ctx, ciphertext + len, &len) != 1) { EVP_CIPHER_CTX_free(enc_ctx); ERR_print_errors_fp(stderr); return EXIT_FAILURE; } ciphertext_len += len; // Release the resources for the context. EVP_CIPHER_CTX_free(enc_ctx); // Showing the ciphertext to stdout. printf("Ciphertext is:\n"); BIO_dump_fp(stdout, (const char *)ciphertext, ciphertext_len); // Initialize a decryption context. The context will contain // all the configurations of the decryption process, such as // the cipher used, the key, etc. EVP_CIPHER_CTX *dec_ctx = EVP_CIPHER_CTX_new(); if (!dec_ctx) { ERR_print_errors_fp(stderr); return EXIT_FAILURE; } // Configure the decryption context by providing the cipher, the // key and the Initialization Vector. if(EVP_DecryptInit(dec_ctx, EVP_aes_256_cbc(), key, iv) != 1) { EVP_CIPHER_CTX_free(dec_ctx); ERR_print_errors_fp(stderr); return EXIT_FAILURE; } // Decrypt the ciphertext, and write the result in the // decrypted_text buffer. Note that this function can be called // multiple times. if(EVP_DecryptUpdate(dec_ctx, decrypted_text, &len, ciphertext, ciphertext_len) != 1) { EVP_CIPHER_CTX_free(dec_ctx); ERR_print_errors_fp(stderr); return EXIT_FAILURE; } decrypted_len = len; // Finalize the decryption process. This function will flush all // the remaining blocks into the decrypted_text buffer. Since you // can call EVP_DecryptUpdate multiple times, OpenSSL is not sure // about the length of the entire ciphertext to remove the // padding. Hence, call this function to inform it that you have // finished providing all the ciphertext, so OpenSSL can finalize // the decryption. Also, len will contain the length of the // decrypted text without including the padding. if(EVP_DecryptFinal(dec_ctx, decrypted_text + len, &len) != 1) { EVP_CIPHER_CTX_free(dec_ctx); ERR_print_errors_fp(stderr); return EXIT_FAILURE; } decrypted_len += len; // Release the resources for the context EVP_CIPHER_CTX_free(dec_ctx); decrypted_text[decrypted_len] = '\0'; // Showing the decrypted message. printf("Decrypted Ciphertext is: %s\n", decrypted_text); return EXIT_SUCCESS; }
The comments in the file provide information about each of steps
involved in the encryption/decryption of a message. To execute this
program, you will have to compile it with a C compiler. Also, you
should link it with the crypto library, namely you should use
-lcrypto option. Assuming the source is in a file named
symmetric.c, the command to compile it in a program is the
following:
$ cc -lcrypto symmetric.c -o symmetric
If you run the resulting binary, you will get an output similar to the following:
$ ./symmetric Ciphertext is: 0000 - 3e d3 e9 c6 54 00 bf 3d-a2 e9 61 7e cb 45 e8 13 >...T..=..a~.E.. 0010 - 52 8c 15 d0 4c f0 17 3f-db 60 d6 d2 3b 76 10 38 R...L..?.`..;v.8 Decrypted Ciphertext is: Some plaintext to encrypt
Note that most probably you will not get the same output for the ciphertext because the key and the IV are randomly generated at the beginning of the program.
Message Digests
By message digests, we refer to result of a cryptographic hash function. A cryptographic hash function is an algorithm that maps an arbitrary size message to a relatively short fixed-size array of bits. A message digest is simply a name for this fixed-size array of bits resulting from a cryptographic hash function. For example, SHA-256 maps an arbitrary size message to a 256-bits array (32 bytes). A cryptographic hash function is good if it has the following properties:
- Collision resistent. It is computationally infeasible finding any two distinct inputs that map to the same output.
- Preimage resistance. Given a randomly chosen output, it is computationally infeasible to find any input that maps to the same output.
- Second preimage resistance. Given one input value, it is computationally infeasible to find a different input value that maps to the same output as the given input value.
Message digests have many important applications, including checking the integrity of a message (think about when you download some software from the Internet, you will often be provided with the digests of the binary), password verification, HMAC codes, blockchains, etc. Let’s now see how OpenSSL can be used to compute message digests. For our demo, we will use the SHA-256 hash function. Let’s first create some data for our digest computation, using the following command:
$ echo "Some data to hash" > data.txt
The OpenSSL utility allows to compute message digests via the dgst
sub-command. As with the enc sub-command, you can specify the
cryptographic hash function to use via options. For SHA-256, you can
use the -sha256 option. Hence, the full command to generate the
digest of our message is the following:
$ openssl dgst -sha256 data.txt
SHA2-256(data.txt)= b7dac3de1fb0db9171f1d21497ebedaf70de5cd8ce186ebec381fc6604e6baa0
Note that if you execute the same commands on your machine, you will get the same output because a cryptographic hash function is also deterministic.
As with the case of symmetric encryption/decryption, the OpenSSL C API also offers a way of computing message digests. The following code shows how to compute the same message digest with the C API:
// Include this header file to use the message digest API. #include <openssl/evp.h> // Include this header file to do error handling. #include <openssl/err.h> // Include this header file to use the SHA family of hash functions. #include <openssl/sha.h> #include <stdio.h> int main(void) { unsigned char data[] = "Some data to hash\n"; // Create a buffer large enough to hold the digest. The digest // has a fixed length depending on the hash function used. In // OpenSSL, the different digest lengths are defined as C macros.b unsigned char digest[SHA256_DIGEST_LENGTH]; // Initialize the hashing context. The hashing context will // contain all the configurations for the hashshing process. EVP_MD_CTX *ctx = EVP_MD_CTX_new(); if (!ctx) { ERR_print_errors_fp(stderr); return EXIT_FAILURE; } // Configure the hashing context to use the SHA-256 hashing // function. if(EVP_DigestInit(ctx, EVP_sha256()) != 1) { EVP_MD_CTX_free(ctx); ERR_print_errors_fp(stderr); return EXIT_FAILURE; } // Provide the data from which we want to compute the digest. // Note that this function can be called multiple times. if(EVP_DigestUpdate(ctx, data, sizeof(data) - 1) != 1) { EVP_MD_CTX_free(ctx); ERR_print_errors_fp(stderr); return EXIT_FAILURE; } // Once you have provided all the data you want to hash, // call the EVP_DigestFinal function to output the resulting // digest into the digest buffer. if(EVP_DigestFinal(ctx, digest, NULL) != 1) { EVP_MD_CTX_free(ctx); ERR_print_errors_fp(stderr); return EXIT_FAILURE; } // Deallocate the resources for the context EVP_MD_CTX_free(ctx); printf("Digest is: "); for (int i = 0; i < SHA256_DIGEST_LENGTH; ++i) printf("%02x", digest[i]); putchar('\n'); }
Here, too, the source file must be compiled and linked using the
-lcrypto option. Assuming that the source is in a file named
hashing.c, the commands to compile it in a program and execute it
are the following:
$ cc -lcrypto hashing.c -o hashing $ ./hashing Digest is: b7dac3de1fb0db9171f1d21497ebedaf70de5cd8ce186ebec381fc6604e6baa0
Again, note that the digest value is the same as the one computed with the CLI utility because cryptographic hash functions are deterministic.
Computing HMAC
A Message Authentication Code (MAC) is a short array of bits that authenticates a message. Message authentication allows a receiver to verify that a message comes from the state sender and that it has not been tampered during transmission. For example, HMAC codes are used in the TLS protocol to authenticate and verify the integrity of protected messages. A Hash-based Message Authentication Code (HMAC) is a MAC generated with HMAC function. The HMAC function simply computes the digest of a message combined some secret key. OpenSSL can also be used to generate a HMAC. For our examples, we will use the SHA-256 hash function. Let’s start by generating some message and a key as we did for the previous examples:
$ echo "Some message to authenticate" > message.txt
$ openssl rand -hex 32
b032e405cd3745d6f9b8315363aeed6ee92b52151703f41ca19bdd3c579b6b40
The OpenSSL dgst sub-command also allows to compute the HMAC for our
data. Again to use SHA-256, we will have to specify the -sha256
option. However, in order to calculate a HMAC, we need to add
-mac HMAC and the -macopt hexkey:<key in hex> options as follows:
$ openssl dgs -mac HMAC \ -mac HMAC \ -macopt hexkey:b032e405cd3745d6f9b8315363aeed6ee92b52151703f41ca19bdd3c579b6b40 \ message.txt HMAC-SHA2-256(message.txt)= c0331da8dea8b050716eb7285bdab144cd09aeb3150ebbd88881a163c65bdb8e
As with the other use cases, also the computation of HMACs can be done via the C API. The following C code shows how this can be done:
// Include this header file to do error handling. #include <openssl/err.h> // Include this header file to use the SHA family of hash functions. #include <openssl/sha.h> // Include this header to use the HMAC function. #include <openssl/hmac.h> #include <stdio.h> int main(void) { unsigned char data[] = "Some message to authenticate\n"; // This is the secret key we will use to compute the HMAC. // Note that this is the same key we generated using the // openssl rand command. unsigned char key[] = { 0xb0, 0x32, 0xe4, 0x05, 0xcd, 0x37, 0x45, 0xd6, 0xf9, 0xb8, 0x31, 0x53, 0x63, 0xae, 0xed, 0x6e, 0xe9, 0x2b, 0x52, 0x15, 0x17, 0x03, 0xf4, 0x1c, 0xa1, 0x9b, 0xdd, 0x3c, 0x57, 0x9b, 0x6b, 0x40 }; // Create a buffer large enough to hold the HMAC. // Since we will be using SHA-256, the length of // the buffer will be the same as the one of a // SHA-256 digest. unsigned char hmac[SHA256_DIGEST_LENGTH]; // Compute the HMAC of the data using the SHA-256 hash function. if (!HMAC(EVP_sha256(), key, sizeof(key), data, sizeof(data) - 1, hmac, NULL)) { ERR_print_errors_fp(stderr); return EXIT_FAILURE; } printf("HMAC is: "); for (int i = 0; i < SHA256_DIGEST_LENGTH; ++i) printf("%02x", hmac[i]); putchar('\n'); }
As before, we need to compile and link the program with the -lcrypto
option. Assuming that the source is into a file named hmac.c, the
commands to compile and execute the program are the following:
$ cc -lcrypto hmac.c -o hmac $ ./hmac HMAC is: c0331da8dea8b050716eb7285bdab144cd09aeb3150ebbd88881a163c65bdb8e
Again, since we did not change the message and the key, the HMAC is the same as the one computed with the CLI utility.
Generating RSA keys
One common use of OpenSSL is generating RSA keys. The genrsa
sub-comand of the openssl utility can be used to generate a RSA
private, as follows:
$ openssl genrsa -out key.pem 2048
The above command will generate an RSA key of size 2048 bits. By a key
of size 2048 bits, we refer to a key whose modulus is 2048 bits
long. From the private key, we can extract the corresponding public
key using the rsa sub-command, as follows:
$ openssl rsa -in key.pem -pubout -out public.pem
If you try to see the content of the two generated files, you will see
some base64 encoded text. In fact, the openssl command outputs the
keys in PEM format which is the base64 encoding of the DER-encoding of
an ASN1 object. ASN1 is a standard description language to describe
the structure for some objects. In fact, many cryptographic structures
are defined with ASN1. You could think of ASN1 as a struct
definition in C. The DER-encoding refers to a binary encoding of this
object, analogous to JSON, which instead is a textual encoding. That
means that even if you do the base64 decoding of the two generated
files, you will not be able to make much sense of them as they will
contain some binary data. However, the openssl command also provides a
way to obtain a human-readable representation of such a structure via
the rsa sub-command using the -text option. Hence, you can
inspect the contents of the private key file with the following
command:
$ openssl rsa -in key.pem -noout -text
Similarly, you can inspect the content of the public file with the following command:
$ openssl rsa -in public.pem -pubin -noout -text
Also the generation of RSA keys can be done using the OpenSSL C API. In fact, the following program shows how it can be done:
// Include this header to use the RSA related functions. #include <openssl/rsa.h> // Include this header file to do error handling. #include <openssl/err.h> // Include this header to use the key high-level interface types. #include <openssl/evp.h> // Include this header use the PEM encoding functions. #include <openssl/pem.h> #include <stdlib.h> #include <stdio.h> int main(void) { // Generate an RSA key of size 2048 bits EVP_PKEY *key = EVP_RSA_gen(2048); if (!key) { ERR_print_errors_fp(stderr); return EXIT_FAILURE; } FILE *fp = fopen("key.pem", "w"); if (!fp) { perror("failed to create key.pem file"); EVP_PKEY_free(key); return EXIT_FAILURE; } // Write the private key in PEM format into the key.pem file PEM_write_PrivateKey(fp, key, NULL, NULL, 0, NULL, NULL); fclose(fp); fp = fopen("public.pem", "w"); if (!fp) { perror("failed to create public.pem file"); EVP_PKEY_free(key); return EXIT_FAILURE; } // Write the public key in PEM format into the public.pem file PEM_write_PUBKEY(fp, key); fclose(fp); EVP_PKEY_free(key); }
As previous examples, this program also requires the -lcrypto option
for linking. Assuming that the code is in a file named rsa.c, the
commands to compile, link and execute the program are the following:
$ cc -lcrypto rsa.c -o rsa $ ./rsa
After executing this program, the working directory should contain two
files key.pem and public.pem.
Generating Certificates
A public key allows to encrypt a message in such a way that only the
owner of the associated private key can decrypt the message. A public
key has the property that it can be shared with others, so it
effectively allows to solve the problem of two parties agreeing on
some shared secret. However, a simple public key does not contain any
information about the entity owning it. In particualar, from a simple
public key you cannot tell if it belongs to google.com or to
usi.ch. X.509 certificates are data structures designed to solve this
issue. In fact, a certificate effectively binds some public key to
some entity. This binding is done via a digital signature operated by
a trusted third party entity, the Certificate Authority (CA). In this
section, we will see how OpenSSL can be used to generate and digitally
sign a certificate. Before generating a certificate, we need to
generate a key, as done in the previous section. Therefore, let’s
generate an RSA key with genrsa as follows:
$ openssl genrsa -out key.pem 2048
After we have a key, we need to generate a Certificate Signing Request (CSR). A CSR is what you would submit to the Certificate Autority for signing. Effectively, the CSR contains the public key and the identity of the requesting entity, but does not contain the signature from the CA (yet). You can use OpenSSL to generate a CSR as follows:
$ openssl req -new \ -key key.pem \ -out cert.csr \ -subj "/C=CH/O=Universita della Svizzera italiana/CN=usi.ch"
Note that the -subj option specifies the identity of the owner of
the public key. As with keys, the CSR is also encoded in PEM, so you
can use the -text option to view the contents of the CSR file. The
command to view the contents of the CSR file is the following:
$ openssl req -in cert.csr -text -noout
Finally, we can sign the request using the req sub-command, as
follows:
$ openssl x509 -req \ -days 365 \ -in cert.csr \ -signkey key.pem \ -out cert.crt
When performing a signature, the CA effectively adds to the
certificate the digest of the CSR signed with its private key. Note
that the signing key we are using is the private key associated with
the public key included in the certificate. The result is a special
kind of certificate known as a self-signed certificate. In particular,
it is not signed by a publicly trusted CA, so it will not be
trusted. To make this certificate trusted, it should be added to the
list of trusted certificates of the host. In fact, all the the
certificates you trust are installed on your host, even the publicly
trusted ones. Often these certificates are installed via packages. For
example, on Debian-based distributions, they are installed via the
ca-certificates package. A certificate is also in PEM format, so
you can view a human-readable representation using the -text
command-line option as follows:
$ openssl x509 -in cert.crt -text -noout
The C API also offers a way to generate and sign certificates. The following code shows how this can be achieved:
// Include this header to use the RSA related functions. #include <openssl/rsa.h> // Include this header file to do error handling. #include <openssl/err.h> // Include this header to use the key high-level interface types. #include <openssl/evp.h> // Include this header use the PEM encoding functions. #include <openssl/pem.h> // Include this header to use the certifcate related functions. #include <openssl/x509.h> #include <stdlib.h> #include <stdio.h> int main(void) { // Let's start by generating a RSA key EVP_PKEY *key = EVP_RSA_gen(2048); if (!key) { ERR_print_errors_fp(stderr); return EXIT_FAILURE; } FILE *fp = fopen("key.pem", "w"); if (!fp) { perror("failed to create key.pem file"); EVP_PKEY_free(key); return EXIT_FAILURE; } // Write the private key in PEM format into the key.pem file PEM_write_PrivateKey(fp, key, NULL, NULL, 0, NULL, NULL); fclose(fp); // Initialize a X509 object that will hold the certificate X509 *cert = X509_new(); if (!cert) { ERR_print_errors_fp(stderr); EVP_PKEY_free(key); return EXIT_FAILURE; } // Set the serial number of a certificate. It is a unique // identifier for different for each certificate signed by a given // CA ASN1_INTEGER_set(X509_get_serialNumber(cert), 1); // Set the time period in which the certificate is valid (same as // the -days option). X509_gmtime_adj(X509_get_notBefore(cert), 0); X509_gmtime_adj(X509_get_notAfter(cert), 60 * 60 * 24 * 365); // Set the public key of the certificate X509_set_pubkey(cert, key); // Set the the values that identify the subject X509_NAME *name = X509_get_subject_name(cert); X509_NAME_add_entry_by_txt(name, "C", MBSTRING_ASC, (unsigned char *) "CH", -1, -1, 0); X509_NAME_add_entry_by_txt(name, "O", MBSTRING_ASC, (unsigned char *) "Universita della Svizzera italiana", -1, -1, 0); X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC, (unsigned char *) "usi.ch", -1, -1, 0); // Sign the certificate with the generated key. Here, we sign the // SHA-256 digest of the certificate with the generated key, X509_sign(cert, key, EVP_sha256()); fp = fopen("cert.crt", "w"); if (!fp) { perror("failed to create cert.crt file"); EVP_PKEY_free(key); X509_free(cert); return EXIT_FAILURE; } // Write the certificate into the cert.crt file PEM_write_X509(fp, cert); fclose(fp); EVP_PKEY_free(key); X509_free(cert); }
Also this program also requires the -lcrypto option
for linking. Assuming that the code is in a file named cert.c, the
commands to compile, link and execute the program are the following:
$ cc -lcrypto cert.c -o cert $ ./cert
After executing this program, the working directory should contain two
files key.pem and cert.crt.
TLS Server/Client
Not only does OpenSSL provide the implementation of many cryptographic algorithms, it also offers an implementation of the TLS protocol. In fact, the SSL in the name refers to the SSL protocol, which was a precursor to TLS.
Given that we have a certificate and the associated private key, we
can start a TLS server using the s_server utility as follows:
$ openssl s_server -cert cert.crt -key key.pem
This command will start listening for TLS connections on port 4433
using the certificate in cert.crt and the key in key.pem. Then, we
can use the s_client sub-command to connect to the listening server
as follows:
$ openssl s_client localhost:4433
The s_client utility is extremely useful also to troubleshoot and
debug TLS connections.
Obviusly, the OpenSSL C API also offers a way run a TLS client and server. After all, OpenSSL is what some browsers use to open TLS connections.
The following code shows how the OpenSSL C API can be used to build a simple TLS server which logs to stdout all the data sent by the client.
#include <stdio.h> #include <unistd.h> #include <signal.h> #include <sys/socket.h> #include <arpa/inet.h> // Include this header to use the TLS related functions. #include <openssl/ssl.h> // Include this header to use the key high-level interface types. #include <openssl/err.h> // This function simply creates a socket via the UNIX socket API and // puts it in listening state. int create_socket(int port) { int s; struct sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_port = htons(port); addr.sin_addr.s_addr = htonl(INADDR_ANY); s = socket(AF_INET, SOCK_STREAM, 0); if (s < 0) { perror("Unable to create socket"); exit(EXIT_FAILURE); } if (bind(s, (struct sockaddr*)&addr, sizeof(addr)) < 0) { perror("Unable to bind"); exit(EXIT_FAILURE); } if (listen(s, 1) < 0) { perror("Unable to listen"); exit(EXIT_FAILURE); } return s; } SSL_CTX *create_context() { const SSL_METHOD *method; SSL_CTX *ctx; // The function tells to starts a version flexible TLS // server. That means that the version will be negitated during // the handshake protocol. method = TLS_server_method(); // This function creates an SSL context which // will hold all the TLS configurations. ctx = SSL_CTX_new(method); if (!ctx) { perror("Unable to create SSL context"); ERR_print_errors_fp(stderr); exit(EXIT_FAILURE); } return ctx; } void configure_context(SSL_CTX *ctx) { // Here we configure the context to use the certificate in the // cert.crt file. if (SSL_CTX_use_certificate_file(ctx, "cert.crt", SSL_FILETYPE_PEM) <= 0) { ERR_print_errors_fp(stderr); exit(EXIT_FAILURE); } // Here we configure the context to use the private key in the // file key.pem file. if (SSL_CTX_use_PrivateKey_file(ctx, "key.pem", SSL_FILETYPE_PEM) <= 0 ) { ERR_print_errors_fp(stderr); exit(EXIT_FAILURE); } } int main(int argc, char **argv) { int sock; SSL_CTX *ctx; signal(SIGPIPE, SIG_IGN); // Create a SSL context and configure it. ctx = create_context(); configure_context(ctx); // Create a socket listening on port 4433. sock = create_socket(4433); while(1) { struct sockaddr_in addr; unsigned int len = sizeof(addr); SSL *ssl; int n; char buf[1024]; // Accept connections from the clients int client = accept(sock, (struct sockaddr*)&addr, &len); if (client < 0) { perror("Unable to accept"); exit(EXIT_FAILURE); } // Create a SSL structure which will hold data for a TLS // connection. It will derive configurations from the input // SSL context. ssl = SSL_new(ctx); // Set the file descriptor for the TLS connection. In this // case, we use the file descriptor of the connecting client // socket. SSL_set_fd(ssl, client); // Wait for the client to initiate the TLS handshake. if (SSL_accept(ssl) <= 0) { ERR_print_errors_fp(stderr); SSL_shutdown(ssl); SSL_free(ssl); close(client); continue; } // Read data from the client. while ((n = SSL_read(ssl, buf, sizeof(buf))) > 0) write(STDOUT_FILENO, buf, n); SSL_shutdown(ssl); SSL_free(ssl); close(client); } close(sock); SSL_CTX_free(ctx); }
Then, we can build a corresponding TLS client as follows:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <netdb.h> #include <sys/socket.h> #include <arpa/inet.h> // Include this header to use the TLS related functions. #include <openssl/ssl.h> // Include this header to use the key high-level interface types. #include <openssl/err.h> SSL_CTX *create_context() { const SSL_METHOD *method; SSL_CTX *ctx; // The function tells to starts a version flexible TLS client, so // it will negotiate the protocol version with the server. method = TLS_client_method(); // This function creates an SSL context which // will hold all the TLS configurations. ctx = SSL_CTX_new(method); if (!ctx) { perror("Unable to create SSL context"); ERR_print_errors_fp(stderr); exit(EXIT_FAILURE); } return ctx; } int main(int argc, char **argv) { int sock; SSL_CTX *ctx; if (argc != 2) { fprintf(stderr, "Usage: %s <server>\n", argv[0]); return EXIT_FAILURE; } // Create a SSL context and configure it. ctx = create_context(); // Create a socket socket and connect to the listening server // socket. struct sockaddr_in addr = { 0 }; addr.sin_family = AF_INET; addr.sin_port = htons(4433); if (inet_pton(AF_INET, argv[1], &addr.sin_addr) <= 0) { struct hostent *entry = gethostbyname(argv[1]); if (!entry) { fputs("could not solve server address", stderr); return EXIT_FAILURE; } memcpy(&addr.sin_addr, entry->h_addr_list[0], sizeof(struct in_addr)); } int sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd < 0) { perror("failed to create socket"); return EXIT_FAILURE; } if (connect(sockfd, (struct sockaddr *) &addr, sizeof(addr)) < 0) { perror("failed to connect to server"); close(sockfd); return EXIT_FAILURE; } int n; char buf[1024]; // Create a SSL structure which will hold data for a TLS // connection. It will derive configurations from the input SSL // context SSL *ssl = SSL_new(ctx); // Set the file descriptor for the TLS connection of the // connecting client socket. SSL_set_fd(ssl, sockfd); // Initiate the TLS handshake if (SSL_connect(ssl) <= 0) { SSL_free(ssl); close(sockfd); SSL_CTX_free(ctx); return EXIT_FAILURE; } // Read data from stdin while ((n = read(STDIN_FILENO, buf, sizeof(buf))) > 0) // Write data back to the client over the TLS connection SSL_write(ssl, buf, n); SSL_shutdown(ssl); SSL_free(ssl); close(sockfd); SSL_CTX_free(ctx); }
Differently from the previous examples, these two programs should be
linked with both the -lcrypto and the -lssl options. Assuming
that the server is in a file named server.c and the client is in a
file named client.c, they can be compiled and linked as follows:
$ cc -lcrypto -lssl server.c -o server $ cc -lcrypto -lssl client.c -o client
If the current working directory contains files cert.crt and
key.pem, you can start the server with the following command:
$ ./server
Finally, the client can connect to the running server and send data that will be be printed on the server stdout as follows:
$ ./client localhost 4433