Skip to content
Home » All Posts » Hands-On OpenSSL C Crypto Examples: AES, RSA, Hash, and HMAC

Hands-On OpenSSL C Crypto Examples: AES, RSA, Hash, and HMAC

Introduction: Why Use OpenSSL for C Cryptography Today

When I write security-critical C code, I treat OpenSSL as my default toolbox for real-world cryptography. It’s widely deployed, battle-tested, and gives me direct access to modern algorithms without having to reinvent low-level crypto myself. In this article of OpenSSL C crypto examples, I’ll focus on the APIs and patterns that have worked reliably for me in production-style code, rather than toy snippets that ignore edge cases.

OpenSSL covers the main building blocks you need for most applications:

  • Symmetric encryption (AES) for fast, bulk data protection
  • Asymmetric cryptography (RSA) for key exchange and signatures
  • Hashing (SHA-256 and friends) for integrity and fingerprinting
  • HMAC for tamper-resistant message authentication

One thing I learned early on was that using the older, algorithm-specific functions (like AES_encrypt or raw RSA_public_encrypt) makes code harder to maintain and port. That’s why all of the OpenSSL C crypto examples here will use the modern EVP APIs (for example, EVP_CIPHER, EVP_MD, EVP_PKEY). These higher-level interfaces are the ones OpenSSL actively recommends, and they map cleanly to current versions such as OpenSSL 1.1.1 and 3.x.

In the sections that follow, I’ll walk through minimal but realistic examples of:

  • AES-GCM encryption/decryption with proper handling of IVs and tags
  • RSA key generation and public-key operations via EVP_PKEY
  • Computing hashes like SHA-256 using EVP_MD
  • Building HMACs for message authentication

To set the tone, here’s a tiny taste of the kind of EVP-based pattern we’ll use repeatedly:

#include <openssl/evp.h>

int hash_sha256(const unsigned char *data, size_t len,
                unsigned char *out, unsigned int *out_len) {
    int ok = 0;
    EVP_MD_CTX *ctx = EVP_MD_CTX_new();
    if (!ctx) return 0;

    if (EVP_DigestInit_ex(ctx, EVP_sha256(), NULL) != 1) goto done;
    if (EVP_DigestUpdate(ctx, data, len) != 1) goto done;
    if (EVP_DigestFinal_ex(ctx, out, out_len) != 1) goto done;

    ok = 1;

done:
    EVP_MD_CTX_free(ctx);
    return ok;
}

This style—create a context, initialize, update, finalize—is the same pattern I rely on for ciphers, hashes, and HMACs. Once you’re comfortable with it, plugging in different algorithms or parameters becomes straightforward.

Prerequisites: Environment, OpenSSL Version, and Build Setup

Required OpenSSL Version and Headers

For the OpenSSL C crypto examples in this guide, I assume you’re using OpenSSL 1.1.1 or 3.x. In my own projects, I’ve standardized on these versions because they expose the modern EVP APIs and hide many legacy internals behind opaque structures.

At a minimum, you should have the development headers and libraries installed. On typical systems, that means:

  • Linux (Debian/Ubuntu): package like libssl-dev
  • Linux (RHEL/CentOS/Fedora): package like openssl-devel
  • macOS: OpenSSL via Homebrew, since the system libraries are outdated for serious work

To confirm which version you’ll be compiling against, I usually run:

openssl version -a

If this reports at least 1.1.1, you’re in good shape for the code we’ll write. Anything older is not worth fighting with for new development.

Compiler Flags and Basic Build Command

Once the headers and libraries are in place, you need to tell your compiler where to find them. On my Linux dev machines, this is typically as simple as adding -lcrypto and sometimes -lssl. For example, to build a file named crypto_example.c:

cc crypto_example.c -o crypto_example \
   -Wall -Wextra -O2 \
   -lcrypto

On systems where OpenSSL is installed in a non-standard prefix (very common with Homebrew or custom builds), you may need explicit -I and -L flags. One habit I picked up is to query pkg-config instead of guessing paths:

cc crypto_example.c -o crypto_example \
   $(pkg-config --cflags --libs openssl)

If pkg-config isn’t set up, tools like openssl enc are a quick way to verify the library itself is working before you even touch C code. For deeper troubleshooting, searching for guidance on OpenSSL build flags for C projects can be very helpful: Compilation and Installation – OpenSSLWiki

Smoke-Test: Minimal Program to Check Headers and Linking

Before investing time into full AES or RSA routines, I like to start with a tiny program that only calls a single OpenSSL function. If this compiles and runs, it confirms your environment, headers, and linker setup are correct.

#include <stdio.h>
#include <openssl/opensslv.h>
#include <openssl/evp.h>

int main(void) {
    printf("OpenSSL version (compile-time): %s\n", OPENSSL_VERSION_TEXT);

    /* Force a reference to EVP to ensure libcrypto is linked correctly */
    const EVP_MD *md = EVP_sha256();
    if (md == NULL) {
        fprintf(stderr, "EVP_sha256() not available. Check your OpenSSL version.\n");
        return 1;
    }

    printf("EVP_sha256() available, environment looks good.\n");
    return 0;
}

Compile it with the same flags you plan to use for the rest of the OpenSSL C crypto examples. If you see the version string and the confirmation message, you’re ready to move on to AES, RSA, hashes, and HMAC implementations.

Prerequisites: Environment, OpenSSL Version, and Build Setup - image 1

OpenSSL EVP Basics: A Unified Interface for Crypto in C

Why EVP Instead of Low-Level APIs

When I first started with OpenSSL, I used the low-level functions like AES_encrypt and RSA_public_encrypt. They worked, but over time I ran into deprecations, awkward parameter handling, and a lot of duplicated logic. Moving to the EVP interface simplified my code and made it future-proof across OpenSSL 1.1.1 and 3.x.

The EVP layer (short for “envelope”) gives you a unified API for different crypto primitives:

  • EVP_CIPHER and EVP_CIPHER_CTX for symmetric encryption like AES-GCM or AES-CBC
  • EVP_MD and EVP_MD_CTX for hashes like SHA-256
  • EVP_PKEY and related functions for RSA and other public-key algorithms

What I like most is that the calling pattern stays almost the same regardless of algorithm. This is why all the OpenSSL C crypto examples in this guide stick to EVP: it’s the API that OpenSSL actively maintains, optimizes, and documents. If you ever need to swap AES-256-GCM for AES-128-GCM, or SHA-256 for SHA-512, you can usually do it by changing a single function like EVP_sha256() to EVP_sha512().

If you want a deeper dive into design rationale and deprecations, it’s worth looking up evp – OpenSSL Documentation. That background helped me avoid using functions that were already on their way out.

The Common EVP Pattern: init → update → final

Across ciphers, hashes, and HMAC, I rely on the same three-step EVP pattern:

  1. Create and initialize a context.
  2. Feed data with one or more Update calls.
  3. Finish with a Final call that produces the output.

This makes the code feel very consistent. Here’s a minimal example I often show teammates to illustrate the core idea using hashing, which we’ll mirror later for AES and HMAC:

#include <stdio.h>
#include <openssl/evp.h>

int main(void) {
    const unsigned char msg[] = "hello evp";
    unsigned char digest[EVP_MAX_MD_SIZE];
    unsigned int digest_len = 0;

    EVP_MD_CTX *ctx = EVP_MD_CTX_new();
    if (!ctx) {
        fprintf(stderr, "Failed to alloc EVP_MD_CTX\n");
        return 1;
    }

    if (EVP_DigestInit_ex(ctx, EVP_sha256(), NULL) != 1 ||
        EVP_DigestUpdate(ctx, msg, sizeof(msg) - 1) != 1 ||
        EVP_DigestFinal_ex(ctx, digest, &digest_len) != 1) {
        fprintf(stderr, "Digest operation failed\n");
        EVP_MD_CTX_free(ctx);
        return 1;
    }

    EVP_MD_CTX_free(ctx);

    printf("SHA-256 length: %u bytes\n", digest_len);
    return 0;
}

In my experience, once you internalize this pattern, the rest of the OpenSSL C crypto examples—AES encryption, RSA sign/verify, and HMAC—feel much less intimidating. You’re mostly reusing the same EVP lifecycle with different types and helper functions.

Symmetric Encryption in C with OpenSSL: AES-GCM Example

Why I Prefer AES-GCM for Modern C Applications

When I’m picking a symmetric cipher for new C projects, I almost always reach for AES-256-GCM. It’s fast on modern CPUs, widely supported, and—most importantly—provides both confidentiality and integrity in one shot. With older modes like CBC, I had to bolt on separate MACs and worry about padding oracles. GCM, when used correctly, removes a lot of that complexity.

In the OpenSSL C crypto examples I use at work, AES-GCM is my default for encrypting structured messages, configuration blobs, and tokens. The key pieces you must handle carefully are:

  • A strong, random 256-bit key
  • A unique IV (nonce) for every encryption (commonly 12 bytes)
  • Optional AAD (Additional Authenticated Data) such as headers you don’t encrypt but want to protect
  • The authentication tag that proves the ciphertext and AAD weren’t tampered with

Once I adopted a simple convention for IV and tag handling, AES-GCM became a very practical workhorse in my C code.

Step-by-Step AES-256-GCM Encrypt and Decrypt with EVP

The EVP pattern for AES-GCM is similar to hashing: create a context, initialize it, process data, then finalize. The main difference is that you also manage IV, AAD, and the tag. Below is a complete example I’ve used as a template in several codebases, trimmed down to the essentials.

#include <stdio.h>
#include <string.h>
#include <openssl/evp.h>

#define AES_KEY_LEN 32      /* 256 bits */
#define AES_IV_LEN  12      /* 96-bit IV is typical for GCM */
#define AES_TAG_LEN 16

int aes_gcm_encrypt(const unsigned char *plaintext, int plaintext_len,
                    const unsigned char *aad, int aad_len,
                    const unsigned char *key,
                    const unsigned char *iv, int iv_len,
                    unsigned char *ciphertext,
                    unsigned char *tag) {
    EVP_CIPHER_CTX *ctx = NULL;
    int len = 0;
    int ciphertext_len = -1;

    ctx = EVP_CIPHER_CTX_new();
    if (!ctx) goto done;

    /* 1. Init context for AES-256-GCM */
    if (EVP_EncryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, NULL, NULL) != 1)
        goto done;

    /* 2. Set IV length if it differs from default (12 bytes is default, but be explicit) */
    if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, iv_len, NULL) != 1)
        goto done;

    /* 3. Now set key and IV */
    if (EVP_EncryptInit_ex(ctx, NULL, NULL, key, iv) != 1)
        goto done;

    /* 4. Provide AAD data (not encrypted but authenticated) */
    if (aad && aad_len > 0) {
        if (EVP_EncryptUpdate(ctx, NULL, &len, aad, aad_len) != 1)
            goto done;
    }

    /* 5. Encrypt plaintext */
    if (EVP_EncryptUpdate(ctx, ciphertext, &len, plaintext, plaintext_len) != 1)
        goto done;
    ciphertext_len = len;

    /* 6. Finalize encryption (no extra bytes for GCM but required) */
    if (EVP_EncryptFinal_ex(ctx, ciphertext + len, &len) != 1)
        goto done;
    ciphertext_len += len;

    /* 7. Get authentication tag */
    if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, AES_TAG_LEN, tag) != 1) {
        ciphertext_len = -1;
        goto done;
    }

.done:
    EVP_CIPHER_CTX_free(ctx);
    return ciphertext_len;  /* -1 on failure */
}

int aes_gcm_decrypt(const unsigned char *ciphertext, int ciphertext_len,
                    const unsigned char *aad, int aad_len,
                    const unsigned char *tag,
                    const unsigned char *key,
                    const unsigned char *iv, int iv_len,
                    unsigned char *plaintext) {
    EVP_CIPHER_CTX *ctx = NULL;
    int len = 0;
    int plaintext_len = -1;
    int ret;

    ctx = EVP_CIPHER_CTX_new();
    if (!ctx) goto done;

    /* 1. Init context for AES-256-GCM */
    if (EVP_DecryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, NULL, NULL) != 1)
        goto done;

    /* 2. Set IV length */
    if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, iv_len, NULL) != 1)
        goto done;

    /* 3. Set key and IV */
    if (EVP_DecryptInit_ex(ctx, NULL, NULL, key, iv) != 1)
        goto done;

    /* 4. Provide AAD data again (must match encrypt side) */
    if (aad && aad_len > 0) {
        if (EVP_DecryptUpdate(ctx, NULL, &len, aad, aad_len) != 1)
            goto done;
    }

    /* 5. Decrypt ciphertext */
    if (EVP_DecryptUpdate(ctx, plaintext, &len, ciphertext, ciphertext_len) != 1)
        goto done;
    plaintext_len = len;

    /* 6. Set expected tag before finalizing */
    if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, AES_TAG_LEN, (void *)tag) != 1) {
        plaintext_len = -1;
        goto done;
    }

    /* 7. Finalize: returns 0 if tag verification fails */
    ret = EVP_DecryptFinal_ex(ctx, plaintext + len, &len);
    if (ret > 0) {
        plaintext_len += len;  /* success */
    } else {
        plaintext_len = -1;    /* authentication failed */
    }

.done:
    EVP_CIPHER_CTX_free(ctx);
    return plaintext_len;  /* -1 on failure or auth error */
}

In my own projects, I wrap these in a small utility module and then define a clear wire format (for example: IV || ciphertext || tag) so every caller uses the same layout. The most important habit is to never reuse an IV with the same key; I usually generate a fresh 96-bit IV using a CSPRNG for each message.

Putting It Together: Minimal Main Function Demo

To make this concrete, here’s a tiny main() I’ve used during debugging sessions to sanity-check that AES-GCM is wired correctly. It encrypts a buffer, then immediately decrypts it and compares the result.

#include <stdio.h>
#include <string.h>
#include <openssl/rand.h>

/* Assume aes_gcm_encrypt / aes_gcm_decrypt are defined above */

int main(void) {
    unsigned char key[AES_KEY_LEN];
    unsigned char iv[AES_IV_LEN];
    unsigned char tag[AES_TAG_LEN];

    const unsigned char aad[] = "example-aad";
    const unsigned char plaintext[] = "Secret message with AES-256-GCM";

    unsigned char ciphertext[sizeof(plaintext) + 16]; /* little extra */
    unsigned char decrypted[sizeof(plaintext) + 16];

    /* Generate random key and IV (in real apps, store the key securely) */
    if (RAND_bytes(key, sizeof(key)) != 1 ||
        RAND_bytes(iv, sizeof(iv)) != 1) {
        fprintf(stderr, "RAND_bytes failed\n");
        return 1;
    }

    int clen = aes_gcm_encrypt(plaintext, (int)strlen((const char *)plaintext),
                               aad, (int)strlen((const char *)aad),
                               key, iv, sizeof(iv),
                               ciphertext, tag);
    if (clen < 0) {
        fprintf(stderr, "Encryption failed\n");
        return 1;
    }

    int plen = aes_gcm_decrypt(ciphertext, clen,
                               aad, (int)strlen((const char *)aad),
                               tag,
                               key, iv, sizeof(iv),
                               decrypted);
    if (plen < 0) {
        fprintf(stderr, "Decryption or authentication failed\n");
        return 1;
    }

    decrypted[plen] = '\0';
    printf("Decrypted: %s\n", decrypted);

    return 0;
}

Running a small demo like this is how I usually validate an AES-GCM implementation before integrating it into a bigger system. Once the round-trip works and authentication failures are handled correctly, I feel confident using it in more critical paths alongside the other OpenSSL C crypto examples like RSA, hashing, and HMAC.

Symmetric Encryption in C with OpenSSL: AES-GCM Example - image 1

Asymmetric Encryption in C with OpenSSL: RSA Key Pair and Usage

Generating an RSA Key Pair with EVP_PKEY

For real applications, I usually prefer to generate RSA keys with openssl CLI or a provisioning service, then just load them in C. But for complete OpenSSL C crypto examples, it’s useful to see how to generate an RSA key pair directly in code using the modern EVP_PKEY APIs. This avoids the older RSA_generate_key_ex style and keeps everything consistent with OpenSSL 1.1.1 and 3.x.

Here’s a minimal example that creates a 2048-bit RSA key pair and holds it in memory. In my own utilities, I typically add extra steps to serialize to PEM, but the core generation logic looks like this:

#include <stdio.h>
#include <openssl/evp.h>
#include <openssl/rsa.h>

EVP_PKEY *generate_rsa_key(int bits) {
    EVP_PKEY *pkey = NULL;
    EVP_PKEY_CTX *ctx = NULL;

    ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, NULL);
    if (!ctx)
        return NULL;

    if (EVP_PKEY_keygen_init(ctx) <= 0)
        goto done;

    if (EVP_PKEY_CTX_set_rsa_keygen_bits(ctx, bits) <= 0)
        goto done;

    if (EVP_PKEY_keygen(ctx, &pkey) <= 0) {
        EVP_PKEY_free(pkey);
        pkey = NULL;
    }

 done:
    EVP_PKEY_CTX_free(ctx);
    return pkey;
}

int main(void) {
    EVP_PKEY *pkey = generate_rsa_key(2048);
    if (!pkey) {
        fprintf(stderr, "RSA key generation failed\n");
        return 1;
    }

    printf("Generated RSA key pair in memory.\n");
    EVP_PKEY_free(pkey);
    return 0;
}

When I first wired this up, the key insight was that EVP_PKEY_CTX drives key generation, and you interact with it using control functions (like setting key size) instead of calling RSA-specific functions directly.

Loading RSA Keys from PEM Files

In production code, I almost always load existing keys from PEM files or in-memory PEM buffers. This keeps key management and application logic separate. OpenSSL makes this straightforward with the PEM_read_bio_PUBKEY and PEM_read_bio_PrivateKey helpers.

Here’s a simple example that loads a private and public key from disk. I’ve used this pattern many times when integrating with services that already have key material provisioned:

#include <stdio.h>
#include <openssl/evp.h>
#include <openssl/pem.h>

EVP_PKEY *load_private_key(const char *path) {
    FILE *fp = fopen(path, "r");
    if (!fp) return NULL;

    EVP_PKEY *pkey = PEM_read_PrivateKey(fp, NULL, NULL, NULL);
    fclose(fp);
    return pkey;  /* NULL on failure */
}

EVP_PKEY *load_public_key(const char *path) {
    FILE *fp = fopen(path, "r");
    if (!fp) return NULL;

    EVP_PKEY *pkey = PEM_read_PUBKEY(fp, NULL, NULL, NULL);
    fclose(fp);
    return pkey;  /* NULL on failure */
}

int main(void) {
    EVP_PKEY *priv = load_private_key("rsa_priv.pem");
    EVP_PKEY *pub  = load_public_key("rsa_pub.pem");

    if (!priv || !pub) {
        fprintf(stderr, "Failed to load RSA keys\n");
        EVP_PKEY_free(priv);
        EVP_PKEY_free(pub);
        return 1;
    }

    printf("Loaded RSA public and private keys from PEM.\n");

    EVP_PKEY_free(priv);
    EVP_PKEY_free(pub);
    return 0;
}

In my experience, the main headache here is file permissions and key formats (PKCS#1 vs PKCS#8). Using PEM_read_PUBKEY and PEM_read_PrivateKey handles most of that for you, as long as the PEM files were created with standard OpenSSL tools.

RSA Encryption and Decryption with EVP_PKEY

Once you have an EVP_PKEY, you can perform RSA encryption and decryption using EVP_PKEY_CTX. In real systems, I often use RSA mostly for key wrapping or signatures and let AES handle the bulk data, but it’s still helpful to see direct RSA encrypt/decrypt as a reference.

The example below demonstrates RSA-OAEP encryption with a public key and decryption with a private key. OAEP is what I reach for by default because it’s more robust than plain PKCS#1 v1.5 padding.

#include <stdio.h>
#include <string.h>
#include <openssl/evp.h>

int rsa_encrypt_oaep(EVP_PKEY *pubkey,
                     const unsigned char *in, size_t in_len,
                     unsigned char *out, size_t *out_len) {
    EVP_PKEY_CTX *ctx = NULL;
    size_t tmp_len = 0;
    int ret = 0;

    ctx = EVP_PKEY_CTX_new(pubkey, NULL);
    if (!ctx) goto done;

    if (EVP_PKEY_encrypt_init(ctx) <= 0) goto done;

    /* Use OAEP with SHA-256 */
    if (EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_OAEP_PADDING) <= 0)
        goto done;
    if (EVP_PKEY_CTX_set_rsa_oaep_md(ctx, EVP_sha256()) <= 0)
        goto done;

    /* 1st call to determine required output length */
    if (EVP_PKEY_encrypt(ctx, NULL, &tmp_len, in, in_len) <= 0)
        goto done;

    if (*out_len < tmp_len) {
        /* caller did not provide enough space */
        *out_len = tmp_len;
        goto done;
    }

    if (EVP_PKEY_encrypt(ctx, out, &tmp_len, in, in_len) <= 0)
        goto done;

    *out_len = tmp_len;
    ret = 1;

 done:
    EVP_PKEY_CTX_free(ctx);
    return ret;
}

int rsa_decrypt_oaep(EVP_PKEY *privkey,
                     const unsigned char *in, size_t in_len,
                     unsigned char *out, size_t *out_len) {
    EVP_PKEY_CTX *ctx = NULL;
    size_t tmp_len = 0;
    int ret = 0;

    ctx = EVP_PKEY_CTX_new(privkey, NULL);
    if (!ctx) goto done;

    if (EVP_PKEY_decrypt_init(ctx) <= 0) goto done;

    if (EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_OAEP_PADDING) <= 0)
        goto done;
    if (EVP_PKEY_CTX_set_rsa_oaep_md(ctx, EVP_sha256()) <= 0)
        goto done;

    /* Determine required buffer size */
    if (EVP_PKEY_decrypt(ctx, NULL, &tmp_len, in, in_len) <= 0)
        goto done;

    if (*out_len < tmp_len) {
        *out_len = tmp_len;
        goto done;
    }

    if (EVP_PKEY_decrypt(ctx, out, &tmp_len, in, in_len) <= 0)
        goto done;

    *out_len = tmp_len;
    ret = 1;

 done:
    EVP_PKEY_CTX_free(ctx);
    return ret;
}

int main(void) {
    const unsigned char msg[] = "RSA with EVP_PKEY and OAEP";
    unsigned char enc[512];
    unsigned char dec[512];
    size_t enc_len = sizeof(enc);
    size_t dec_len = sizeof(dec);

    EVP_PKEY *keypair = generate_rsa_key(2048);
    if (!keypair) {
        fprintf(stderr, "Key generation failed\n");
        return 1;
    }

    if (!rsa_encrypt_oaep(keypair, msg, sizeof(msg) - 1, enc, &enc_len)) {
        fprintf(stderr, "Encryption failed\n");
        EVP_PKEY_free(keypair);
        return 1;
    }

    if (!rsa_decrypt_oaep(keypair, enc, enc_len, dec, &dec_len)) {
        fprintf(stderr, "Decryption failed\n");
        EVP_PKEY_free(keypair);
        return 1;
    }

    dec[dec_len] = '\0';
    printf("Decrypted message: %s\n", dec);

    EVP_PKEY_free(keypair);
    return 0;
}

One thing I learned the hard way is to always check buffer sizes and return codes for every EVP call; with RSA, a silent size mismatch can easily turn into confusing failures later. In real deployments, I combine this pattern with the AES-GCM example from earlier: RSA wraps a random symmetric key, and AES-GCM carries the actual payload. That hybrid approach balances security with performance while staying entirely within the EVP APIs recommended for modern OpenSSL C crypto examples.

If you want more background on padding options and interoperability concerns, it’s worth searching for OpenSSL RSA encryption and decryption with OAEP padding – OpenSSL Wiki. Those resources helped me standardize parameters between C code and other languages.

Hashing in C with OpenSSL: SHA-256 Digest Example

Hashing In-Memory Buffers with EVP_Digest*

For many of my day-to-day tasks—like fingerprinting configuration blobs or verifying small payloads—I just need a quick SHA-256 over a memory buffer. The EVP digest APIs make this very straightforward and follow the same init → update → final pattern used elsewhere in these OpenSSL C crypto examples.

Here’s a compact helper that computes a SHA-256 digest for a given buffer and prints it as hex. I’ve used almost this exact snippet in small utilities and test harnesses:

#include <stdio.h>
#include <openssl/evp.h>

int sha256_buffer(const unsigned char *data, size_t len,
                  unsigned char *out, unsigned int *out_len) {
    EVP_MD_CTX *ctx = EVP_MD_CTX_new();
    if (!ctx) return 0;

    if (EVP_DigestInit_ex(ctx, EVP_sha256(), NULL) != 1 ||
        EVP_DigestUpdate(ctx, data, len) != 1 ||
        EVP_DigestFinal_ex(ctx, out, out_len) != 1) {
        EVP_MD_CTX_free(ctx);
        return 0;
    }

    EVP_MD_CTX_free(ctx);
    return 1;
}

static void print_hex(const unsigned char *buf, unsigned int len) {
    for (unsigned int i = 0; i < len; i++)
        printf("%02x", buf[i]);
    printf("\n");
}

int main(void) {
    const unsigned char msg[] = "hash me with sha-256";
    unsigned char digest[EVP_MAX_MD_SIZE];
    unsigned int dlen = 0;

    if (!sha256_buffer(msg, sizeof(msg) - 1, digest, &dlen)) {
        fprintf(stderr, "SHA-256 failed\n");
        return 1;
    }

    printf("SHA-256: ");
    print_hex(digest, dlen);
    return 0;
}

In my experience, separating the generic sha256_buffer helper from the hex-printing makes it easy to reuse the hashing logic across different tools and tests.

Streaming SHA-256 for Files and Large Data

For larger inputs—log files, backups, or streamed network data—I lean on the same EVP digest API but feed data in chunks. This is one of the reasons I like EVP: I don’t need a different interface for streaming versus in-memory hashing.

Here’s a practical example that hashes a file in blocks. I’ve adapted this pattern for integrity checks on deployment artifacts and backup archives:

#include <stdio.h>
#include <openssl/evp.h>

int sha256_file(const char *path, unsigned char *out, unsigned int *out_len) {
    FILE *fp = fopen(path, "rb");
    if (!fp) return 0;

    EVP_MD_CTX *ctx = EVP_MD_CTX_new();
    if (!ctx) {
        fclose(fp);
        return 0;
    }

    if (EVP_DigestInit_ex(ctx, EVP_sha256(), NULL) != 1) {
        EVP_MD_CTX_free(ctx);
        fclose(fp);
        return 0;
    }

    unsigned char buf[4096];
    size_t n;
    while ((n = fread(buf, 1, sizeof(buf), fp)) > 0) {
        if (EVP_DigestUpdate(ctx, buf, n) != 1) {
            EVP_MD_CTX_free(ctx);
            fclose(fp);
            return 0;
        }
    }

    if (ferror(fp)) {
        EVP_MD_CTX_free(ctx);
        fclose(fp);
        return 0;
    }

    if (EVP_DigestFinal_ex(ctx, out, out_len) != 1) {
        EVP_MD_CTX_free(ctx);
        fclose(fp);
        return 0;
    }

    EVP_MD_CTX_free(ctx);
    fclose(fp);
    return 1;
}

int main(int argc, char **argv) {
    if (argc != 2) {
        fprintf(stderr, "Usage: %s <file>\n", argv[0]);
        return 1;
    }

    unsigned char digest[EVP_MAX_MD_SIZE];
    unsigned int dlen = 0;

    if (!sha256_file(argv[1], digest, &dlen)) {
        fprintf(stderr, "Failed to hash file %s\n", argv[1]);
        return 1;
    }

    printf("SHA-256(%s) = ", argv[1]);
    for (unsigned int i = 0; i < dlen; i++)
        printf("%02x", digest[i]);
    printf("\n");

    return 0;
}

One thing I like about this pattern is how closely it mirrors the earlier EVP examples: same context, same sequence of calls, just a different data source. Once you’re comfortable with SHA-256 in this form, moving between hashing, HMAC, and even AES in the rest of these OpenSSL C crypto examples feels very natural.

Hashing in C with OpenSSL: SHA-256 Digest Example - image 1

HMAC in C with OpenSSL: Authenticating Messages with SHA-256

Why HMAC-SHA-256 Instead of a Plain Hash

When I need to make sure a message hasn’t been tampered with and really came from someone who knows a secret key, I reach for HMAC-SHA-256. A plain SHA-256 hash only protects against random corruption; anyone can recompute it. HMAC, on the other hand, mixes in a secret key with the data in a way that’s resistant to common attacks, so only someone with that key can produce a valid tag.

In practice, I use HMAC-SHA-256 to protect API requests, small configuration records, and sometimes as a building block in higher-level protocols. It fits nicely alongside the other OpenSSL C crypto examples: AES-GCM gives me authenticated encryption, while HMAC-SHA-256 gives me just authentication when I don’t need encryption.

OpenSSL offers both a dedicated HMAC_* API and an EVP-based interface. I tend to start people on the simpler HMAC() helpers, then move to the streaming EVP style when they need more flexibility or already use EVP elsewhere.

Creating and Verifying HMAC-SHA-256 Tags in C

Here’s a straightforward way to compute an HMAC-SHA-256 tag over a memory buffer using the high-level HMAC() function. This is often all I need for small messages or quick integrity checks during debugging.

#include <stdio.h>
#include <string.h>
#include <openssl/hmac.h>
#include <openssl/evp.h>

int hmac_sha256_buffer(const unsigned char *key, size_t key_len,
                       const unsigned char *data, size_t data_len,
                       unsigned char *out, unsigned int *out_len) {
    unsigned char *res = HMAC(EVP_sha256(), key, (int)key_len,
                              data, data_len, out, out_len);
    return res != NULL;
}

static void print_hex(const unsigned char *buf, unsigned int len) {
    for (unsigned int i = 0; i < len; i++)
        printf("%02x", buf[i]);
    printf("\n");
}

int main(void) {
    const unsigned char key[] = "super-secret-key";   /* use random bytes in real code */
    const unsigned char msg[] = "message to authenticate";

    unsigned char tag[EVP_MAX_MD_SIZE];
    unsigned int tag_len = 0;

    if (!hmac_sha256_buffer(key, strlen((const char *)key),
                            msg, strlen((const char *)msg),
                            tag, &tag_len)) {
        fprintf(stderr, "HMAC-SHA-256 failed\n");
        return 1;
    }

    printf("HMAC-SHA-256: ");
    print_hex(tag, tag_len);
    return 0;
}

To verify a received tag, I recompute the HMAC over the same message with the same key and then compare the results in constant time. One of the early mistakes I made was using memcmp(), which can leak timing information; these days I either implement a simple constant-time comparison or reuse one from a shared utility module.

For streaming or multi-part messages, I prefer the EVP-based HMAC interface since it mirrors the digest and cipher patterns you’ve already seen in this guide:

#include <openssl/hmac.h>

int hmac_sha256_stream(const unsigned char *key, size_t key_len,
                       const unsigned char *part1, size_t part1_len,
                       const unsigned char *part2, size_t part2_len,
                       unsigned char *out, unsigned int *out_len) {
    HMAC_CTX *ctx = HMAC_CTX_new();
    if (!ctx) return 0;

    if (HMAC_Init_ex(ctx, key, (int)key_len, EVP_sha256(), NULL) != 1 ||
        HMAC_Update(ctx, part1, part1_len) != 1 ||
        HMAC_Update(ctx, part2, part2_len) != 1 ||
        HMAC_Final(ctx, out, out_len) != 1) {
        HMAC_CTX_free(ctx);
        return 0;
    }

    HMAC_CTX_free(ctx);
    return 1;
}

In my experience, once teams see how similar this looks to EVP_Digest* and the other OpenSSL C crypto examples, they stop treating HMAC as something special or scary—it’s just another EVP-style primitive with a secret key mixed in. For more nuanced protocol design (sequence numbers, replay protection, etc.), it’s worth looking into HMAC usage with OpenSSL – OpenSSL Wiki so you don’t reinvent insecure message formats.

Putting It Together: A Small OpenSSL C Crypto Toolkit

Designing a Simple Command-Line Crypto Utility

Once I had working OpenSSL C crypto examples for AES-GCM, RSA, hashing, and HMAC, the next natural step was to wrap them into a small command-line tool. That way, I could quickly test behaviors, debug interoperability with other languages, and hand teammates a single binary instead of loose snippets.

The pattern that has worked well for me is a single crypto-tool executable with subcommands such as:

  • aes-gcm-encrypt / aes-gcm-decrypt – encrypt/decrypt files with a symmetric key
  • rsa-encrypt / rsa-decrypt – wrap small secrets with an RSA public/private key pair
  • sha256 – hash a file or string
  • hmac-sha256 – compute or verify a message authentication tag

Internally, each subcommand just calls into the helper functions we built earlier in this guide. Structuring it this way made it easy for me to later refactor those helpers into a reusable static library for other projects.

Example: Wiring Subcommands to the Crypto Primitives

Here’s a stripped-down main.c that shows how I glue the primitives together behind a simple CLI. The real versions I use have more error handling and file I/O, but this captures the core idea:

#include <stdio.h>
#include <string.h>
#include <openssl/evp.h>
#include <openssl/rand.h>

/* Assume implementations from earlier sections exist:
 *  - aes_gcm_encrypt / aes_gcm_decrypt
 *  - generate_rsa_key, rsa_encrypt_oaep, rsa_decrypt_oaep
 *  - sha256_buffer, sha256_file
 *  - hmac_sha256_buffer
 */

static void usage(const char *prog) {
    fprintf(stderr,
        "Usage:\n"
        "  %s sha256 <string>\n"
        "  %s aes-gcm-demo\n"
        "  %s rsa-demo\n"
        "  %s hmac-demo\n",
        prog, prog, prog, prog);
}

int cmd_sha256(int argc, char **argv) {
    if (argc < 3) {
        fprintf(stderr, "sha256 requires a string argument\n");
        return 1;
    }

    const unsigned char *msg = (const unsigned char *)argv[2];
    unsigned char digest[EVP_MAX_MD_SIZE];
    unsigned int dlen = 0;

    if (!sha256_buffer(msg, strlen((const char *)msg), digest, &dlen)) {
        fprintf(stderr, "SHA-256 failed\n");
        return 1;
    }

    printf("SHA-256: ");
    for (unsigned int i = 0; i < dlen; i++)
        printf("%02x", digest[i]);
    printf("\n");
    return 0;
}

int cmd_aes_gcm_demo(void) {
    const unsigned char aad[] = "cli-aad";
    const unsigned char plaintext[] = "CLI AES-GCM test";

    unsigned char key[32];
    unsigned char iv[12];
    unsigned char tag[16];
    unsigned char ciphertext[128];
    unsigned char decrypted[128];

    RAND_bytes(key, sizeof key);
    RAND_bytes(iv, sizeof iv);

    int clen = aes_gcm_encrypt(plaintext, (int)strlen((const char *)plaintext),
                               aad, (int)strlen((const char *)aad),
                               key, iv, sizeof iv,
                               ciphertext, tag);
    if (clen < 0) {
        fprintf(stderr, "AES-GCM encrypt failed\n");
        return 1;
    }

    int plen = aes_gcm_decrypt(ciphertext, clen,
                               aad, (int)strlen((const char *)aad),
                               tag, key, iv, sizeof iv,
                               decrypted);
    if (plen < 0) {
        fprintf(stderr, "AES-GCM decrypt failed or auth error\n");
        return 1;
    }

    decrypted[plen] = '\0';
    printf("AES-GCM round-trip: %s\n", decrypted);
    return 0;
}

int cmd_hmac_demo(void) {
    const unsigned char key[] = "demo-hmac-key";
    const unsigned char msg[] = "demo-message";
    unsigned char tag[EVP_MAX_MD_SIZE];
    unsigned int tag_len = 0;

    if (!hmac_sha256_buffer(key, strlen((const char *)key),
                            msg, strlen((const char *)msg),
                            tag, &tag_len)) {
        fprintf(stderr, "HMAC-SHA-256 failed\n");
        return 1;
    }

    printf("HMAC tag: ");
    for (unsigned int i = 0; i < tag_len; i++)
        printf("%02x", tag[i]);
    printf("\n");
    return 0;
}

int cmd_rsa_demo(void) {
    const unsigned char msg[] = "rsa-cli-test";
    unsigned char enc[512];
    unsigned char dec[512];
    size_t enc_len = sizeof enc;
    size_t dec_len = sizeof dec;

    EVP_PKEY *keypair = generate_rsa_key(2048);
    if (!keypair) {
        fprintf(stderr, "RSA keygen failed\n");
        return 1;
    }

    if (!rsa_encrypt_oaep(keypair, msg, sizeof(msg) - 1, enc, &enc_len) ||
        !rsa_decrypt_oaep(keypair, enc, enc_len, dec, &dec_len)) {
        fprintf(stderr, "RSA encrypt/decrypt failed\n");
        EVP_PKEY_free(keypair);
        return 1;
    }

    dec[dec_len] = '\0';
    printf("RSA round-trip: %s\n", dec);

    EVP_PKEY_free(keypair);
    return 0;
}

int main(int argc, char **argv) {
    if (argc < 2) {
        usage(argv[0]);
        return 1;
    }

    if (strcmp(argv[1], "sha256") == 0)
        return cmd_sha256(argc, argv);
    if (strcmp(argv[1], "aes-gcm-demo") == 0)
        return cmd_aes_gcm_demo();
    if (strcmp(argv[1], "rsa-demo") == 0)
        return cmd_rsa_demo();
    if (strcmp(argv[1], "hmac-demo") == 0)
        return cmd_hmac_demo();

    usage(argv[0]);
    return 1;
}

When I first built a toolkit like this, the biggest benefit wasn’t just having a demo program—it was forcing myself to think about keys, inputs, and outputs in a consistent way across AES, RSA, hashing, and HMAC. That consistency made it much easier to later promote these OpenSSL C crypto examples into a small internal library other teams could depend on.

Putting It Together: A Small OpenSSL C Crypto Toolkit - image 1

Conclusion and Next Steps for Secure OpenSSL C Crypto Examples

What You’ve Built and Where to Go From Here

By this point, you’ve assembled a solid, hands-on toolkit of OpenSSL C crypto examples: AES-256-GCM for authenticated encryption, RSA key generation and OAEP encryption with EVP_PKEY, streaming SHA-256 digests, and HMAC-SHA-256 for message authentication, all wrapped in a small CLI-friendly structure. In my own projects, this is roughly the point where simple experiments start turning into reusable internal libraries.

The key habits you’ve practiced—using EVP instead of low-level APIs, never reusing AES-GCM IVs, preferring OAEP for RSA, checking every return code, and keeping keys separate from data paths—carry directly into larger systems. From here, natural next steps include exploring OpenSSL Documentation, where the same EVP primitives are orchestrated into full secure channels. You can also look into PKCS#11-backed key storage, hardware acceleration, and integrating your mini-toolkit into CI pipelines for integrity checks and test fixtures.

Join the conversation

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