Cryptographic keys and SSL encryption

  • by

What is a Cryptographic Key?

In cryptography, a key is a string of characters used in a cryptographic algorithm to alter(lock) the data in such a way that it can be decrypted(unlocked) only by someone with the right key. 

encrypt(“Hello world!”, key) = “U2FsdGVkX18zIUJRbr2nYpavPTPcDp4WxVV5X4qmwf4=” 

The original data is named in cryptographic terms “plain text”, while the encrypted(locked) plain text is called cyphertext. 

Historical Background

Before the era of computers, the ciphertext was quite simple compared to its modern counterparts, mainly they were based on substitution and shifting the plaintext by a certain number of characters. Few famous examples of ciphertext include: 

 

The Caesar Shift Cipher 

Its first appearance is dated around the first Century AD in the Roman Empire, the chipper was named in honor of Julius Caesar which according to the historian Suetonius used it to encrypt secret messages. Due to the low level of literacy among the citizens of the roman empire the cipher remained secure during the roman era, after the demise of the roman empire, around 9th century AD, historical records exist which document multiple ways to crack it using frequency analysis from Al-Kindi. 

 

Scytale 

Was a simple form of cryptography used in antiquity in Greece, Scytale is a simple tool of cylindrical form with a strip of parchment wound around it on  which a message is written, used to form a transposition cipher, it was mainly used to communicate during military campaigns. The Recipient of  the message would use a rod of exact same length on which it would wrap  the message in order to decrypt it. 

 

Steganography 

It is a method of hiding messages in plain text disguised as something else, the earliest record which documents the usage of this method was described by Herodotus in the Histories. Herodotus describes how the leader of the Ionian city of Miletus, in the late century BC, sent a message to one of his vassals by shaving the head of his most trusted servant and marking the message on onto his scalp, and then letting the slaves heir regrow, on arrival to the destination the servant was ordered to shave again such that the recipient can “decrypt” the message.

 

Key types

Similarly to physical keys for locks, cryptographic keys are made for specific proposes, in general they are divided in two categories, namely symmetric and asymmetric, the former always is used as a pair of mathematically related pair, the private and the public key.

 

Symmetric and Asymmetric Keys

Symmetric and asymmetric keys are best understood in relation to the encryption and decryption algorithms that are using them. Symmetric algorithms use the same key for encryption and decryption of the data, in general symmetric encryption algorithms  are not computationally demanding, their main disadvantage lays in the fact that the key must be exchanged between the communicating ends, thus increasing the risk that an intruder might intercept the key. Asymmetric algorithms use a pair of keys, the public key is used for encryption while the private key is used for decryption, once the message is encrypted with the public key it can be decrypted only with the private key, it works similar to a physical key-lock pair, the lock can be used to close a box containing a secret message, but once closed, it can be opened only with the key.

In the following I will demonstrate few examples of symmetric and asymmetric encryption using the C API of OpenSSL, as a build system for this demo app I will be using CMake.

Simple RSA Encryption and Decryption Program

The following program was built on Ubuntu 20.04.2 LTS with the software library OpenSSL 1.1.1f released in 31 Mar 2020. It was built and compiled in CMake version 3.16.3. Down bellow you can see the CMake file that was used to build:

				
					# specify that the minimum required version of cmake is 3.10
cmake_minimum_required(VERSION 3.10)

# set the project name
project(RSA_CIPHER VERSION 1.0)

# specify that the project is in C++ version 11      
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED True)

# create executable files from the project files
add_executable(RSA_CIPHER main.cpp)

# tell cmake where to find the headers from the target directory
target_include_directories(RSA_CIPHER PUBLIC
                           "${PROJECT_BINARY_DIR}"
                           )

# link the project with the OpenSSL libraries                         
find_package(OpenSSL REQUIRED)
target_link_libraries(RSA_CIPHER OpenSSL::SSL)
				
			

The program was written in C++ and its purpose is to demonstrate the usage of the OpenSSL library in a simple program for encrypting and decrypting a plain text with the RSA encryption algorithm. First off we generate the RSA public and private key.

				
					RSA *generated_key = RSA_generate_key(2048, 3, NULL, NULL);
				
			

The first parameter is the key length that should not be higher than 4096 bits or lower than 1024 bits and the second parameter is the key exponent that is an odd number.

Then you need to extract the public and private key out of the generated key.

				
					BIO *pri = BIO_new(BIO_s_mem());
BIO *pub = BIO_new(BIO_s_mem());

PEM_write_bio_RSAPrivateKey(pri, generated_key, NULL, NULL, 0, NULL, NULL);
PEM_write_bio_RSAPublicKey(pub, generated_key);

private_key_size = BIO_pending(pri);
public_key_size = BIO_pending(pub);
	
private_key = (char*)malloc(private_key_size + 1);
public_key = (char*)malloc(public_key_size + 1);

BIO_read(pri, private_key, private_key_size);
BIO_read(pub, public_key, public_key_size);
				
			

Firstly we create and initialize 2 BIO memory buffers, in which we write the public and private key parts of the RSA generated key. Then we access the BIO streams and return the amount of pending data from the read and write buffers. We then allocate 2 buffers that are supposed to hold the public and the private key parts. Then we read the public and private key data that is in the BIO buffers and place them in the allocated buffers for each key. Now that we have both keys we can attempt to start the encryption.

				
					int encrypt_size = RSA_public_encrypt(strlen(message) + 1, 
                    (unsigned char*)message, (unsigned char*)encrypt, 
                    generated_key, RSA_PKCS1_OAEP_PADDING);
				
			

We start the encryption by using the plain text and its length, a buffer to store the encryption(encrypt) data and the public key. After running the part from above we receive the encrypted text also known as the cypher text and the size of it. As for the decryption we do the same thing but reversed with the cypher text instead of the plain text.

				
					int decrypt_size = RSA_private_decrypt(encrypt_size, 
                    (unsigned char*)encrypt, (unsigned char*)decrypt,
                    generated_key, RSA_PKCS1_OAEP_PADDING);
				
			

The decryption begins by using the cypher text and its length, a buffer to store the decryption(decrypt) data and the private key. The result of the part shown above is the decrypted message and the size of it. After each operation of encryption and decryption we verify if the cipher text and decrypted text is correct, if not we receive the error and the program is terminated.

				
					if(encrypt_size  == -1) {
	ERR_load_crypto_strings();
    ERR_error_string(ERR_get_error(), errors);
	cout << "Encryption error = " << errors << endl;
	exit (EXIT_FAILURE);
}
				
			

We do that by verifying the encryption and decryption size. If the process failed firstly we load the the errors from libcrypto and then we print out the first error encountered in a human readable form. Down bellow you can see the full code of the program.

				
					#include <iostream>
#include <openssl/bio.h> // used for creating Basic Input/Output streams
#include <openssl/err.h> // used to handle errors
#include <openssl/ssl.h> // OpenSSL library
#include <openssl/rsa.h> // RSA algorithm library
#include <openssl/pem.h> // used to write in the BIO memory
#include <string.h>
#include <stdlib.h>

using namespace std;

int main() {
	
	int private_key_size;
	int public_key_size;
	char *private_key;
	char *public_key;
	char message[2048/8];
	char *encrypt;
	char *decrypt;
	char *errors;

	// generate a key that is stored in the RSA structure
	RSA *generated_key = RSA_generate_key(2048, 3, NULL, NULL);

	// create and initialize a new BIO memory
	BIO *pri = BIO_new(BIO_s_mem());
	// use a memory BIO for Input/Output data
	BIO *pub = BIO_new(BIO_s_mem());

	/* write in the allocated BIO memory the public 
	and private parts of the RSA key */
	/* third parameter is used mostly for symmetric encryption, 
	but since we do asymetric encryption we assigned it to NULL */
	// fourth parameter would be a passphrase
	// fifth parameter would be the passphrase length
	// sixth parameter would be the callback
	/* seventh parameter is the passphrase 
	but since we are not using password protected keys we assigned it to NULL*/
	PEM_write_bio_RSAPrivateKey(pri, generated_key, NULL, NULL, 0, NULL, NULL);
    PEM_write_bio_RSAPublicKey(pub, generated_key);

	/* access the BIO stream and return the amount of pending data
	from the read and write buffers */
	private_key_size = BIO_pending(pri);
    public_key_size = BIO_pending(pub);
	
	// allocate buffers for holding the private and public key
	private_key = (char*)malloc(private_key_size + 1);
    public_key = (char*)malloc(public_key_size + 1);

	/* read the private and public key data from the BIO structures 
	and put it in the allocated buffers */
	BIO_read(pri, private_key, private_key_size);
    BIO_read(pub, public_key, public_key_size);

	cout << "Private Key = " << private_key << endl;
	cout << "Public Key = " << public_key << endl;

	string msg = "they are coming from the east";
	// convert plaintext into a char array
	strcpy(message, msg.c_str());
	// allocate buffer for holding the encrypted data
	encrypt = (char*)malloc(RSA_size(generated_key));
	
	/* take the generated key, plaintext, size of the plaintext, encrypt buffer 
	then encrypt the plaintext and store it in the buffer encrypt 
	and return the size of the encrypted message / chiphertext */
	int encrypt_size = RSA_public_encrypt(strlen(message) + 1, 
	                    (unsigned char*)message, (unsigned char*)encrypt,
                        generated_key, RSA_PKCS1_OAEP_PADDING);

	// allocate a buffer for storing the error logs 
	errors = (char*)malloc(130);
	/* verify if the encryption size is correct, 
	if not give an error and end the program */
	if(encrypt_size  == -1) {
		// load the errors from libcrypto
		ERR_load_crypto_strings();
		/* parse the errors buffer and print out the first encountered error
		in a human readable format */
        ERR_error_string(ERR_get_error(), errors);
		cout << "Encryption error = " << errors << endl;
		exit (EXIT_FAILURE);
	}

	cout << "Encrypted Message = ";
	for(int i = 0; i < strlen(encrypt); i++)
    	cout << hex << (int)encrypt[i];
	cout << dec << sizeof(encrypt) << RSA_size(generated_key) << endl;

	// allocate a buffer for holding encryption size
	decrypt = (char*)malloc(encrypt_size);

	/* take the encryption size, ciphertext, generated key, decrypt buffer
	then decrypt the message and store it in the buffer decrypt 
	and return the size of the decrypted message */
	int decrypt_size = RSA_private_decrypt(encrypt_size, 
	                    (unsigned char*)encrypt, (unsigned char*)decrypt,
                        generated_key, RSA_PKCS1_OAEP_PADDING);
	/* verify if the decryption size is correct, 
	if not give an error and terminate the program	*/			   
	if(decrypt_size == -1) {
		ERR_load_crypto_strings();
        ERR_error_string(ERR_get_error(), errors);
		cout << "Decryption error = " << errors << endl;
		exit (EXIT_FAILURE);
	}

	cout << "Decrypted Message = " << decrypt << endl;

	// free the allocated memory
	free(private_key);
	free(public_key);
	free(encrypt);
	free(decrypt);
	free(errors);
	RSA_free(generated_key);
    BIO_free_all(pub);
    BIO_free_all(pri);
	
	return 0;
}
				
			

Lastly we would want to free the allocated memory. In our program since we used an older version of the OpenSSL C API the following functions: RSA_generate_key() PEM_write_bio_RSAPublicKey() and PEM_write_bio_RSAPrivateKey() are deprecated in the latest version, instead we would advise the usage of the following functions: RSA_generate_key_ex(), OSSL_ENCODER_to_bio() and OSSL_DECODER_from_bio().

 

Client Socket with TLS Encryption

In the next example I will program a client socket type wrapped in TLS encryption, I will print the encryption used and the client certificate. Then I will verify if the client certificate is trusted and lastly print out a part of the source code from Google webpage where the client will be connected to. 

The first thing that I would need to do is to prepare the TLS “wrap” for the socket, I do that by creating a TLS Method for client socket type. Then I create a SSL context based on the TLS Method.

				
					const SSL_METHOD *ssl_method = TLS_client_method();
SSL_CTX *ssl_context = SSL_CTX_new(ssl_method);
if(ssl_context == NULL) {
    ERR_print_errors_fp(stderr);
    exit(EXIT_FAILURE);
}
				
			

After I verify if the context was created I will create a SSL structure that holds the data for the TLS connection, then I will verify if the structure was created.

				
					SSL *tls = SSL_new(ssl_context);
if(tls == NULL) {
    cout << "Cannot create SSL Structure" << endl;
    exit(EXIT_FAILURE);
}
				
			

Now that I have the TLS “wrap” I need to create a client socket that connects to Google. First I need to check if the host and its port exist, if not the program closes, I do that by creating a structure that receives the information from the host and then check the information.

				
					struct hostent *remote_host;
remote_host = gethostbyname(ip);
if(remote_host == NULL) {
    perror(ip);
    exit(EXIT_FAILURE);
}
				
			

I create a second structure similar to the one I already have but instead I will hint it the information about the server. The hinted information is if the IP of the server is IPv4 or IPv6, the socket type and the protocol for the returned socket addresses. I receive again information about the server and verify if it’s correct, if not the program closes.

				
					struct addrinfo hinter = {0}, *address;
hinter.ai_family = AF_UNSPEC;
hinter.ai_socktype = SOCK_STREAM;
hinter.ai_protocol = IPPROTO_TCP;
const int status = getaddrinfo(ip, port, &hinter, &address);
if(status != 0) {
    cout << gai_strerror(status) << endl;
    exit(EXIT_FAILURE);
}
				
			

Then I access the information about the server and make sure the connection is successful even if the host has multiple internet addresses(mirrors). I create a socket with the host data and attempt to connect the socket to the server. If the connection failed I will receive the error information and then close the socket. After I attempted the connection I free the allocated memory for the address and check if the socket connection was successful, if not the error is displayed followed by the program termination.

				
					for(struct addrinfo *addr = address; addr != NULL; addr = addr->ai_next) {
    socketfd = socket(address->ai_family, address->ai_socktype, 
                        address->ai_protocol);
    
    if (socketfd == -1) {
        error = errno;
        continue;
    }

    if(connect(socketfd, addr->ai_addr, addr->ai_addrlen) == 0) {
        cout << "Connected to " << ip << ":" << port << endl;
        break;
    }
 
    error = errno;
    socketfd = -1;
    close(socketfd);
}

freeaddrinfo(address);

if(socketfd == -1) {
    cout << "Connection failed to " << ip << ":" << port << 
            " with the error: " << strerror(error) << endl;
    exit(EXIT_FAILURE);
}
				
			

Now that I have the client socket connected to the Google webpage, I need to secure it with the TLS “wrap”, create a TLS handshake with the server and check if the handshake was successful, if not it prints the error and closes the program. Then I display the cipher used for the encryption.

				
					SSL_set_fd(tls, socket);

const int status = SSL_connect(tls);
if(status != 1) {
    SSL_get_error(tls, status);
    ERR_print_errors_fp(stderr);
    cout << "Could not connect to SSL, Error Code = " << status << endl;
    exit(EXIT_FAILURE);
}

cout << "The cipher used for the connection is " << 
        SSL_get_cipher(tls) << endl;
				
			

Since the connection is secure I display the client certificate and verify if the certificate is trusted, if not the program closes. Then I display the client certificate verification and the status if there were any error in the verification.

				
					X509 *certificate = SSL_get_peer_certificate(tls);
X509_print_fp(stdout, certificate);

if(!SSL_CTX_load_verify_locations(ssl_context,
                                "/etc/ssl/certs/ca-certificates.crt",
                                "/etc/ssl/certs/"))
{
    cout << "Client Certificate is not in the truststore" << endl;
    exit(EXIT_FAILURE);
}

long ver_status = SSL_get_verify_result(tls);
if (ver_status != X509_V_OK)
    cout << "\tClient Certificate Verification Error = " << 
            ver_status << endl << "\t\tResuming" << endl; 
else
    cout << "\tClient Certificate Verification" << endl;
				
			

Then I create the request command for the server that asks for the source code of the Google webpage. I write the command in the socket, then read a part of the source code since the source code is a huge wall of text and display it for the user to see it.

				
					sprintf(request,
"GET / HTTP/1.1\x0D\x0AHost: %s\x0D\x0A\x43onnection: Close\x0D\x0A\x0D\x0A",
        "www.google.com");
    
SSL_write(tls, request, 1024);
SSL_read(tls, source, 1024);
cout << source << endl;
				
			

Lastly the connection needs to be closed and free the allocated memory for the TLS and the SSL Context.

				
					SSL_free(tls);

cout << "Disconnected" << endl;
close(socket);

SSL_CTX_free(ssl_context);
				
			

Down bellow you can see the full code of this program.

				
					#include <iostream>
#include <string.h>
#include <errno.h> // used in case of an error to list it
#include <unistd.h> // used to close the socket
#include <netdb.h> /* used to store host information, connect the socket
and for connection error handling */
#include <openssl/ssl.h> // OpenSSL library
#include <openssl/err.h> // used to handle OpenSSL errors

using namespace std;
 
// create socket connection function
int open_socket(const char *ip, const char *port) {
    // create a structure that holds various information about the host
    struct hostent *remote_host;
    // receive the information about the host
    remote_host = gethostbyname(ip);
    // verify if the host exists, if not send error and exit the program
    if(remote_host == NULL) {
        perror(ip);
        exit(EXIT_FAILURE);
    }
 
    // create a different structure that holds information about the host
    struct addrinfo hinter = {0}, *address;
    // specify the IP type
    hinter.ai_family = AF_UNSPEC;
    // specify the socket type
    hinter.ai_socktype = SOCK_STREAM;
    // specify the protocol type
    hinter.ai_protocol = IPPROTO_TCP;
 
    // receive the information about the host
    const int status = getaddrinfo(ip, port, &hinter, &address);
    // verify if the host exists, else send error and close the program
    if(status != 0) {
        cout << gai_strerror(status) << endl;
        exit(EXIT_FAILURE);
    }
    int socketfd, error;
    // access the information about the host
    for(struct addrinfo *addr = address; addr != NULL; addr = addr->ai_next) {
        // create the socket
        socketfd = socket(address->ai_family, address->ai_socktype,
                            address->ai_protocol);
        if (socketfd == -1) {
            error = errno;
            continue;
        }
        // attempt to open the socket
        if(connect(socketfd, addr->ai_addr, addr->ai_addrlen) == 0) {
            cout << "Connected to " << ip << ":" << port << endl;
            break;
        }
 
        error = errno;
        
        socketfd = -1;
        // close the socket if operation failed
        close(socketfd);
    }
 
    // free the allocated memory for the address
    freeaddrinfo(address);
    // if the socket failed to connect send error and terminate the program
    if(socketfd == -1) {
        cout << "Connection failed to " << ip << ":" << port << 
                " with the error: " << strerror(error)<< endl;
        exit(EXIT_FAILURE);
    }
    return socketfd;
}
 
int main() {
    const char *host = "www.google.com";
    const char *port = "443";

    // create a tls method for a client socket
    // indicate that the program is client that supports tls
    const SSL_METHOD *ssl_method = TLS_client_method();

    // create a ssl context based on the chosen ssl method
    SSL_CTX *ssl_context = SSL_CTX_new(ssl_method);

    // check if ssl_context was created, else send error and close the program
    if(ssl_context == NULL) {
        ERR_print_errors_fp(stderr);
        exit(EXIT_FAILURE);
    }

    // create ssl stucture that holds the data for the tls connection
    SSL *tls = SSL_new(ssl_context);

    // check if the ssl structure was created, if not stop the program
    if(tls == NULL) {
        cout << "Cannot create SSL Structure" << endl;
        exit(EXIT_FAILURE);
    }
 
    int socket;
    cout << "Connecting . . ." << endl;
    // open socket using a host and a port
    socket = open_socket(host, port);
    
    // secure the socket connection with tls
    SSL_set_fd(tls, socket);
 
    // create tls handshake with the server
    const int status = SSL_connect(tls);
    /* verify if the handshake was succesful, else provide the errors 
    and terminate the program */
    if(status != 1) {
        SSL_get_error(tls, status);
        ERR_print_errors_fp(stderr);
        cout << "Could not connect to SSL, Error Code = " << status << endl;
        exit(EXIT_FAILURE);
    }
 
    cout << "The cipher used for the connection is " << 
            SSL_get_cipher(tls) << endl;
    
    // print the client certificate
    X509 *certificate = SSL_get_peer_certificate(tls);
    X509_print_fp(stdout, certificate);

    // verify if the client certificate is within the truststore
    if(!SSL_CTX_load_verify_locations(ssl_context,
                                      "/etc/ssl/certs/ca-certificates.crt",
                                      "/etc/ssl/certs/"))
    {
        cout << "Client Certificate is not in the truststore" << endl;
        exit(EXIT_FAILURE);
    }
    
    // check for certificate verification errors
    long ver_status = SSL_get_verify_result(tls);
    if (ver_status != X509_V_OK)
        cout << "\tClient Certificate Verification Error = " << 
                ver_status << endl << "\t\tResuming" << endl; 
    else
        cout << "\tClient Certificate Verification" << endl;

    char source[1024];
    char request[1024];

    // receive the source code of the host
    sprintf(request,
"GET / HTTP/1.1\x0D\x0AHost: %s\x0D\x0A\x43onnection: Close\x0D\x0A\x0D\x0A",
          "www.google.com");
    
    SSL_write(tls, request, 1024);
    SSL_read(tls, source, 1024);
    cout << source << endl;
 
    // free allocated memory for tls
    SSL_free(tls);

    cout << "Disconnected" << endl;
    close(socket);

    // free allocated memory for the ssl context
    SSL_CTX_free(ssl_context);
 
    return 0;
}
				
			

Leave a Reply

Your email address will not be published. Required fields are marked *