Best LWC Algorithm: ECDH vs RSA Comparison

Protonest IoT
9 min readMay 24, 2024

--

In the world of cryptography, choosing the right algorithm is crucial, especially for IoT systems where power, processing resources, and bandwidth are limited. Two prominent contenders in this space are RSA (Rivest-Shamir-Adleman) and ECDH (Elliptic Curve Diffie-Hellman). This article does a comparative analysis of these algorithms, focusing on key generation, shared key exchange, throughput, security, and power consumption to help you identify the best Lightweight Cryptographic (LWC) algorithm for your IoT system.

Our Experiment

We used the code for the comparison

#include <iostream>
#include "sodium.h"
#include <openssl/ssl.h>
#include <openssl/crypto.h>
#include <openssl/err.h>
#include <openssl/rsa.h>
#include <openssl/bn.h>
#include <openssl/kdf.h>
#include <openssl/evp.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <tchar.h>
#include <chrono>

#pragma comment ( lib, "Crypt32.lib" )

using namespace std;

#define CONTEXT "DataTrnf" // 8 character string that explains what the key is going to be used for

const char* hostname = "example.com"; // Replace with your server's hostname, add the same host name manually in line 107
const int port = 443; // Standard port for HTTPS

// Function prototypes

void key_gen(unsigned char* privateKey, unsigned char* publicKey);
void cal_shared_key(unsigned char* sharedSecret, unsigned char* otherPublicKey, unsigned char* privateKey);
void key_exchange(unsigned char* localPublicKey, unsigned char* peerPublicKey);
int openConnection(const char* hostname, int port);
SSL* createSSLConnection(SSL_CTX* ctx, int sock);
void print_key(unsigned char* string);
void PrintRSAPublicKeyComponents(RSA* rsa);
void PrintRSAPrivateKeyComponents(RSA* rsa);
int encrypt_symmetric_key_with_rsa_public_key(unsigned char* symmetric_key, size_t symmetric_key_length, RSA* public_rsa, unsigned char** encrypted_data, size_t* encrypted_data_length);
int decrypt_symmetric_key_with_rsa_private_key(unsigned char* encrypted_data, size_t encrypted_data_length, RSA* private_rsa, unsigned char** symmetric_key, size_t* symmetric_key_length);
int derive_key_from_shared_secret(unsigned char* shared_secret, size_t shared_secret_length, unsigned char** derived_key, size_t* derived_key_length);

int main()
{
////////////////////////////// Initialization //////////////////////////////

// initialize sodium library
if (sodium_init() < 0) {
/* The library couldn't be initialized; it is not safe to use */
cerr << "Libsodium Initialization Failed." << endl;
return 1;
}

// initialize SSL/TLS library
SSL_load_error_strings();
OpenSSL_add_ssl_algorithms();

////////////////////////////// Key Pair Generation //////////////////////////////

// Variables for local keypair
unsigned char publicKey[crypto_box_PUBLICKEYBYTES];
unsigned char privateKey[crypto_box_SECRETKEYBYTES];

auto startKeyGen = std::chrono::high_resolution_clock::now();

key_gen(publicKey, privateKey); // generate the private and public key pair

auto endKeyGen = std::chrono::high_resolution_clock::now();
std::chrono::duration<double, std::milli> keyGenDuration = endKeyGen - startKeyGen;
cout << "ECDH Key Generation Duration: " << keyGenDuration.count() << " ms" << endl;

// RAS Key Generation
auto startKeyGenRSA = std::chrono::high_resolution_clock::now();
RSA* rsa_keypair = RSA_new();
BIGNUM* bn_e = BN_new();
BN_set_word(bn_e, RSA_F4); // RSA_F4 = 65537 is a common public exponent
RSA_generate_key_ex(rsa_keypair, 2048, bn_e, nullptr);
auto endKeyGenRSA = std::chrono::high_resolution_clock::now();
std::chrono::duration<double, std::milli> keyGenDurationRSA = endKeyGenRSA - startKeyGenRSA;
cout << "RAS Key Generation Duration: " << keyGenDurationRSA.count() << " ms" << endl;

cout << endl;

////////////////////////////// Key Exchange //////////////////////////////

// unsigned char peerPublicKey[crypto_box_SECRETKEYBYTES]; // variable to store external public key
// key_exchange(publicKey, peerPublicKey); // exchange keys over TLS channel

////////////////////////////// Secret Key Calculation //////////////////////////////

// For demonstration
unsigned char peerPublicKey[crypto_box_PUBLICKEYBYTES];
unsigned char peerPrivateKey[crypto_box_SECRETKEYBYTES];

key_gen(peerPublicKey, peerPrivateKey);

auto startSharedKeyCal = std::chrono::high_resolution_clock::now();

unsigned char sharedSecret[crypto_scalarmult_BYTES]; // variable to store the shared key

// Load the shared key into the variable and check its return status
cal_shared_key(sharedSecret, privateKey, peerPublicKey);

auto endSharedKeyCal = std::chrono::high_resolution_clock::now();
std::chrono::duration<double, std::milli> sharedKeyCalDuration = endSharedKeyCal - startSharedKeyCal;
cout << "ECDH Shared Key Calculation (Decryption) Duration: " << sharedKeyCalDuration.count() << " ms" << endl;

// RSA Shared key calculation
// Symmetric key to be encrypted (example)
unsigned char symmetric_key[] = "ExampleSymmetricKey123"; // Normally, you'd generate this key randomly
size_t symmetric_key_length = strlen((char*)symmetric_key);

// Encrypt the symmetric key using RSA public key
unsigned char* encrypted_data = nullptr;
size_t encrypted_data_length = 0;
encrypt_symmetric_key_with_rsa_public_key(symmetric_key, symmetric_key_length, rsa_keypair, &encrypted_data, &encrypted_data_length);

// Decrypt the symmetric key using RSA private key
unsigned char* decrypted_symmetric_key = nullptr;
size_t decrypted_symmetric_key_length = 0;

auto startSharedKeyCalRSA = std::chrono::high_resolution_clock::now();
decrypt_symmetric_key_with_rsa_private_key(encrypted_data, encrypted_data_length, rsa_keypair, &decrypted_symmetric_key, &decrypted_symmetric_key_length);
auto endSharedKeyCalRSA = std::chrono::high_resolution_clock::now();
std::chrono::duration<double, std::milli> sharedKeyCalDurationRSA = endSharedKeyCalRSA - startSharedKeyCalRSA;
cout << "RSA Shared Key Calculation (Decryption) Duration: " << sharedKeyCalDurationRSA.count() << " ms" << endl;

cout << endl;

////////////////////////////// Key Derivation //////////////////////////////
auto startKeyDer = std::chrono::high_resolution_clock::now();

unsigned char encryptionKey[crypto_kdf_KEYBYTES]; // define the desired key size
crypto_kdf_derive_from_key(encryptionKey, sizeof(encryptionKey), 1, CONTEXT, sharedSecret); // calculate the derived key

auto endKeyDer = std::chrono::high_resolution_clock::now();
std::chrono::duration<double, std::milli> keyDerDuration = endKeyDer - startKeyDer;
cout << "ECDH Key Derivation Duration: " << keyDerDuration.count() << " ms" << endl;

// RSA Key Derivation
// Derive a new key from the decrypted symmetric key
unsigned char* derived_key = nullptr;
size_t derived_key_length = 0;

auto startKeyDerRSA = std::chrono::high_resolution_clock::now();
derive_key_from_shared_secret(decrypted_symmetric_key, decrypted_symmetric_key_length, &derived_key, &derived_key_length);
auto endKeyDerRSA = std::chrono::high_resolution_clock::now();
std::chrono::duration<double, std::milli> keyDerDurationRSA = endKeyDerRSA - startKeyDerRSA;
cout << "RSA Key Derivation Duration: " << keyDerDurationRSA.count() << " ms" << endl;

cout << endl;

cout << "ECDH Total Duration: " << (keyGenDuration.count() + sharedKeyCalDuration.count() + keyDerDuration.count()) << " ms" << endl;
cout << "RSA Total Duration: " << (keyGenDurationRSA.count() + sharedKeyCalDurationRSA.count() + keyDerDurationRSA.count()) << " ms" << endl;

return 0;
}

// Function to print the RSA Public Key components (n and e)
void PrintRSAPublicKeyComponents(RSA* rsa) {
const BIGNUM* n = nullptr, * e = nullptr;
// Get the public key components
RSA_get0_key(rsa, &n, &e, nullptr);

// Convert the BIGNUMs to hexadecimal strings
char* n_hex = BN_bn2hex(n);
char* e_hex = BN_bn2hex(e);

// Print the components
std::cout << "RSA Public Key Components:" << std::endl;
std::cout << "n (modulus): " << (n_hex ? n_hex : "Unavailable") << std::endl;
std::cout << "e (public exponent): " << (e_hex ? e_hex : "Unavailable") << std::endl;

// Free the hexadecimal strings
OPENSSL_free(n_hex);
OPENSSL_free(e_hex);
}

// Function to print the RSA Private Key components (d, p, and q)
void PrintRSAPrivateKeyComponents(RSA* rsa) {
const BIGNUM* d = nullptr, * p = nullptr, * q = nullptr;
// Get the private key components
RSA_get0_key(rsa, nullptr, nullptr, &d);
RSA_get0_factors(rsa, &p, &q);

// Convert the BIGNUMs to hexadecimal strings
char* d_hex = BN_bn2hex(d);
char* p_hex = BN_bn2hex(p);
char* q_hex = BN_bn2hex(q);

// Print the components
std::cout << "RSA Private Key Components:" << std::endl;
std::cout << "d (private exponent): " << (d_hex ? d_hex : "Unavailable") << std::endl;
std::cout << "p (prime 1): " << (p_hex ? p_hex : "Unavailable") << std::endl;
std::cout << "q (prime 2): " << (q_hex ? q_hex : "Unavailable") << std::endl;

// Free the hexadecimal strings
OPENSSL_free(d_hex);
OPENSSL_free(p_hex);
OPENSSL_free(q_hex);
}

int derive_key_from_shared_secret(unsigned char* shared_secret, size_t shared_secret_length, unsigned char** derived_key, size_t* derived_key_length) {
EVP_PKEY_CTX* pctx = EVP_PKEY_CTX_new_id(EVP_PKEY_HKDF, NULL);
if (!pctx) return -1;

if (EVP_PKEY_derive_init(pctx) <= 0) return -2;

// Setting HKDF mode and shared info (if any)
if (EVP_PKEY_CTX_set_hkdf_md(pctx, EVP_sha256()) <= 0) return -3;
if (EVP_PKEY_CTX_set1_hkdf_salt(pctx, reinterpret_cast<const unsigned char*>("salt"), strlen("salt")) <= 0) return -4; // Example salt
if (EVP_PKEY_CTX_set1_hkdf_key(pctx, shared_secret, shared_secret_length) <= 0) return -5;

// Deriving the key
if (EVP_PKEY_derive(pctx, NULL, derived_key_length) <= 0) return -6;
*derived_key = (unsigned char*)OPENSSL_malloc(*derived_key_length);
if (!*derived_key) return -7;
if (EVP_PKEY_derive(pctx, *derived_key, derived_key_length) <= 0) return -8;

EVP_PKEY_CTX_free(pctx);
return 0;
}

int encrypt_symmetric_key_with_rsa_public_key(unsigned char* symmetric_key, size_t symmetric_key_length, RSA* public_rsa, unsigned char** encrypted_data, size_t* encrypted_data_length) {
// Determine the size of the buffer for the encrypted data
int rsa_size = RSA_size(public_rsa);
*encrypted_data = (unsigned char*)malloc(rsa_size);
if (!*encrypted_data) {
return -1; // Memory allocation failed
}

// Encrypt the symmetric key with the public RSA key
int result = RSA_public_encrypt(symmetric_key_length, symmetric_key, *encrypted_data, public_rsa, RSA_PKCS1_OAEP_PADDING);
if (result < 0) {
// Encryption failed
free(*encrypted_data);
*encrypted_data = NULL;
return -2;
}

*encrypted_data_length = result;
return 0; // Success
}

int decrypt_symmetric_key_with_rsa_private_key(unsigned char* encrypted_data, size_t encrypted_data_length, RSA* private_rsa, unsigned char** symmetric_key, size_t* symmetric_key_length) {
// Allocate a buffer for the decrypted symmetric key
*symmetric_key = (unsigned char*)malloc(RSA_size(private_rsa));
if (!*symmetric_key) {
return -1; // Memory allocation failed
}

// Decrypt the symmetric key with the private RSA key
int result = RSA_private_decrypt(encrypted_data_length, encrypted_data, *symmetric_key, private_rsa, RSA_PKCS1_OAEP_PADDING);
if (result < 0) {
// Decryption failed
free(*symmetric_key);
*symmetric_key = NULL;
return -2;
}

*symmetric_key_length = result;
return 0; // Success
}

// Generate the private and public key pair
void key_gen(unsigned char* privateKey, unsigned char* publicKey){

crypto_box_curve25519xsalsa20poly1305_keypair(publicKey, privateKey); // generate key pair using the
//cout << "Private Key: ";
//print_key(privateKey);
//cout << "Public Key: ";
//print_key(publicKey);
}

// Calculate shared key for a given local private key and a peer's public key
void cal_shared_key(unsigned char* sharedSecret, unsigned char* otherPublicKey, unsigned char* privateKey) {

crypto_scalarmult(sharedSecret, privateKey, otherPublicKey); // load the shared key into the variable
}

// Open a TCP connection to the specified hostname and port
int openConnection(const char* hostname, int port) {

WSADATA wsaData;
SOCKET sock = INVALID_SOCKET;
struct sockaddr_in server;

// Initialize Winsock library, used for network communications
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
cerr << "WSAStartup failed.\n";
exit(3);
}

// Create a socket for the TCP connection
sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock == INVALID_SOCKET) {
cerr << "Socket creation failed.\n";
WSACleanup();
exit(4);
}

// Setup server address structure
// Add the hostname manually here
InetPton(AF_INET, _T("example.com"), &server.sin_addr.s_addr); // Set server IP address
server.sin_family = AF_INET; // Use IPv4 address family
server.sin_port = htons(port); // Set server port, converting to network byte order

// Establish a connection to the server
if (connect(sock, (struct sockaddr*)&server, sizeof(server)) < 0) {
cerr << "Connect error.\n";
closesocket(sock);
WSACleanup();
exit(5);
}

return (int)sock; // Return the socket descriptor for the established connection
}

// Wrap the TCP connection with an SSL layer and establish an SSL connection
SSL* createSSLConnection(SSL_CTX* ctx, int sock) {

// Create a new SSL structure for the connection
SSL* ssl = SSL_new(ctx);
if (!ssl) {
std::cerr << "SSL_new failed.\n";
closesocket(sock);
WSACleanup();
exit(6);
}

SSL_set_fd(ssl, sock); // Associate the network socket with the SSL structure
if (SSL_connect(ssl) != 1) { // Establish SSL connection
SSL_free(ssl); // Free the SSL structure
closesocket(sock); // Close the socket
WSACleanup();
std::cerr << "SSL_connect failed.\n";
exit(7);
}

return ssl; // Return the SSL structure representing the established connection
}

void key_exchange(unsigned char* localPublicKey, unsigned char* peerPublicKey) {

// Create a new SSL context with default settings for a client
const SSL_METHOD* method = TLS_client_method(); // Use TLS_server_method() for server
SSL_CTX* ctx = SSL_CTX_new(method);

SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, nullptr);
SSL_CTX_load_verify_locations(ctx, "path/to/ca_cert.pem", nullptr);

int sock = openConnection(hostname, port); // Open a TCP connection to the server
SSL* ssl = createSSLConnection(ctx, sock); // Establish an SSL connection over the TCP socket

// Send your public key
SSL_write(ssl, localPublicKey, sizeof(localPublicKey));

// Read peer's public key
int bytesRead = SSL_read(ssl, peerPublicKey, sizeof(peerPublicKey));
if (bytesRead <= 0) {
cerr << "Receiving Peer Public Key UNSUCCESSFUL" << endl;
}
}

void print_key(unsigned char* string) {
// Output the keys in hexadecimal form (for demonstration purposes)
for (unsigned int i = 0; i < sizeof(string); i++) {
printf("%02x", string[i]);
}
std::cout << std::endl;
}

Results we got

Key Takeaways

  • ECDH is approximately 60 times faster than RSA in key generation, enhancing system responsiveness during initiation and key renewal.
  • ECDH is about 10 times faster than RSA in the shared key calculation, facilitating quicker secure communication.
  • ECDH’s total operational time is significantly lower than RSA, making it more suitable for environments with limited computational resources.

Security

Cryptographic Strength

  • ECDH: The security of ECDH is based on the elliptic curve discrete logarithm problem, which is sub-exponential, approximately √2𝑛. This allows ECDH to achieve high security with shorter key lengths.
  • RSA: The security of RSA relies on the difficulty of factoring large integers. The effort required increases with key size but not as sharply as ECDH.

Key Size Comparison

  • To achieve equivalent security, ECDH requires significantly shorter keys compared to RSA. For instance, a 160-bit ECDH key is roughly equivalent to a 1024-bit RSA key.

Implications

  • The shorter key lengths in ECDH translate to lower computational overhead, making it ideal for bandwidth and resource-constrained environments like IoT.

Power Consumption

Measured by PrimeTime (Synopsys)

  • Diffie-Hellman: 0.832 mW
  • RSA: 1.512 mW
  • ECDH: 0.570 mW

Measured by Xilinx Xpower

  • Diffie-Hellman: 6.0 mW
  • RSA: 11.0 mW
  • ECDH: 5.0 mW

Insights

  • ECDH demonstrates the lowest power consumption across both evaluation tools, proving its suitability for power-sensitive applications.

Conclusion

In comparing RSA and ECDH for IoT systems, ECDH emerges as the superior Lightweight Cryptographic (LWC) algorithm. Its significantly lower delays in key generation and shared key calculation, combined with its efficiency in power consumption and high-security levels with shorter key lengths, make it an excellent choice for IoT environments.

References

  • T. Goyal and V. Sahula, “Lightweight Security Algorithm for Low Power IoT,” International Conference on Advances in Computing, Communications and Informatics (ICACCI), pp. 1725–1729, 2016.

Contact us for any consultations or projects related to lightweight cryptographic algorithms for IoT systems.

Protonest for more details.

Protonest specializes in transforming IoT ideas into reality. We offer prototyping services from concept to completion. Our commitment ensures that your visionary IoT concepts become tangible, innovative, and advanced prototypes.

LinkedIn: https://www.linkedin.com/company/protonest/

Website: https://www.protonest.co/

Email: udara@protonest.co

If you enjoyed this article and would like to show some support, consider buying me a coffee on Ko-Fi.

Your support not only helps me keep creating content like this but also fuels me! ☕️📚

Thank you for being an amazing reader!

Here’s my Ko-Fi link: https://ko-fi.com/udarakumarasena

Cheers!

--

--