Skip to content
C++
Domain Track
Difficulty 3/5

Cryptography in C++

C++ cryptography — OpenSSL hashing, encryption/decryption, random bytes, HMAC, TLS with Asio, and libsodium for modern crypto.

TL;DR

Use libsodium for new code — simple, hard to misuse. Use OpenSSL when you need protocol-level TLS control. Never implement your own crypto. Always use secure random for keys and nonces.


Hashing with OpenSSL (EVP API)

cpp
#include <openssl/evp.h>
#include <vector>
#include <string>

std::vector<uint8_t> sha256(std::string_view data) {
    unsigned int len = 0;
    std::vector<uint8_t> hash(EVP_MAX_MD_SIZE);

    EVP_MD_CTX* ctx = EVP_MD_CTX_new();
    EVP_DigestInit_ex(ctx, EVP_sha256(), nullptr);
    EVP_DigestUpdate(ctx, data.data(), data.size());
    EVP_DigestFinal_ex(ctx, hash.data(), &len);
    EVP_MD_CTX_free(ctx);

    hash.resize(len);  // 32 bytes for SHA-256
    return hash;
}

// Hex encoding
std::string to_hex(std::span<const uint8_t> bytes) {
    std::string hex;
    hex.reserve(bytes.size() * 2);
    for (uint8_t b : bytes)
        std::format_to(std::back_inserter(hex), "{:02x}", b);
    return hex;
}

// Usage
auto hash = sha256("hello world");
std::println("SHA-256: {}", to_hex(hash));
// SHA-256: b94d27b9934d3e08a52e52d7da7dabfac484efe04294e576b7...

HMAC-SHA256

For message authentication (verify data was not tampered with):

cpp
#include <openssl/hmac.h>

std::vector<uint8_t> hmac_sha256(std::string_view key, std::string_view data) {
    unsigned int len = 0;
    std::vector<uint8_t> result(EVP_MAX_MD_SIZE);

    HMAC(EVP_sha256(),
         key.data(), static_cast<int>(key.size()),
         reinterpret_cast<const uint8_t*>(data.data()), data.size(),
         result.data(), &len);

    result.resize(len);
    return result;
}

// Constant-time comparison (prevent timing attacks)
bool secure_equals(std::span<const uint8_t> a, std::span<const uint8_t> b) {
    if (a.size() != b.size()) return false;
    return CRYPTO_memcmp(a.data(), b.data(), a.size()) == 0;
}

// Verify
auto expected_mac = hmac_sha256("secret-key", "data");
auto received_mac = /* from request */;
if (secure_equals(expected_mac, received_mac))
    std::println("valid");

Symmetric Encryption: AES-256-GCM

Authenticated encryption — provides confidentiality + integrity:

cpp
#include <openssl/evp.h>
#include <stdexcept>

struct AesGcmCiphertext {
    std::vector<uint8_t> nonce;    // 12 bytes
    std::vector<uint8_t> tag;      // 16 bytes
    std::vector<uint8_t> ciphertext;
};

AesGcmCiphertext aes256gcm_encrypt(
    std::span<const uint8_t> key,    // must be 32 bytes
    std::span<const uint8_t> plaintext)
{
    AesGcmCiphertext result;
    result.nonce.resize(12);
    RAND_bytes(result.nonce.data(), 12);  // random nonce — never reuse!

    result.ciphertext.resize(plaintext.size());
    result.tag.resize(16);

    EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();
    EVP_EncryptInit_ex(ctx, EVP_aes_256_gcm(), nullptr, nullptr, nullptr);
    EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, 12, nullptr);
    EVP_EncryptInit_ex(ctx, nullptr, nullptr, key.data(), result.nonce.data());

    int out_len;
    EVP_EncryptUpdate(ctx, result.ciphertext.data(), &out_len,
                      plaintext.data(), plaintext.size());

    EVP_EncryptFinal_ex(ctx, result.ciphertext.data() + out_len, &out_len);
    EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, 16, result.tag.data());
    EVP_CIPHER_CTX_free(ctx);

    return result;
}

Secure Random Numbers

cpp
#include <openssl/rand.h>

// Cryptographically secure random bytes
std::vector<uint8_t> random_bytes(size_t n) {
    std::vector<uint8_t> buf(n);
    if (RAND_bytes(buf.data(), static_cast<int>(n)) != 1)
        throw std::runtime_error("RAND_bytes failed");
    return buf;
}

// Generate a 256-bit key
auto key = random_bytes(32);

// Generate a UUID v4 (using OpenSSL random)
std::string uuid_v4() {
    auto bytes = random_bytes(16);
    bytes[6] = (bytes[6] & 0x0F) | 0x40;  // version 4
    bytes[8] = (bytes[8] & 0x3F) | 0x80;  // variant RFC 4122
    return std::format(
        "{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-"
        "{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
        bytes[0], bytes[1], bytes[2], bytes[3],
        bytes[4], bytes[5], bytes[6], bytes[7],
        bytes[8], bytes[9], bytes[10], bytes[11],
        bytes[12], bytes[13], bytes[14], bytes[15]);
}

libsodium — Modern Simple API

libsodium is easier to use correctly than OpenSSL:

cpp
#include <sodium.h>

// Initialize (call once at startup)
if (sodium_init() < 0) throw std::runtime_error("libsodium init failed");

// Symmetric encryption: XSalsa20-Poly1305 (authenticated)
void secret_box_example() {
    // Generate key
    uint8_t key[crypto_secretbox_KEYBYTES];
    crypto_secretbox_keygen(key);

    // Generate nonce (random, unique per message)
    uint8_t nonce[crypto_secretbox_NONCEBYTES];
    randombytes_buf(nonce, sizeof(nonce));

    std::string message = "secret message";
    std::vector<uint8_t> ciphertext(crypto_secretbox_MACBYTES + message.size());

    crypto_secretbox_easy(ciphertext.data(),
        reinterpret_cast<const uint8_t*>(message.data()), message.size(),
        nonce, key);

    // Decrypt + verify
    std::vector<uint8_t> plaintext(message.size());
    if (crypto_secretbox_open_easy(plaintext.data(), ciphertext.data(),
                                   ciphertext.size(), nonce, key) != 0)
        throw std::runtime_error("decryption failed or tampered");
}

// Password hashing (Argon2id)
void password_hash_example() {
    char hash[crypto_pwhash_STRBYTES];
    std::string password = "user-password";

    if (crypto_pwhash_str(hash, password.data(), password.size(),
                          crypto_pwhash_OPSLIMIT_INTERACTIVE,
                          crypto_pwhash_MEMLIMIT_INTERACTIVE) != 0)
        throw std::runtime_error("OOM");

    // Verify
    bool valid = crypto_pwhash_str_verify(
        hash, password.data(), password.size()) == 0;
}

Public-Key Cryptography

Key generation and signing (libsodium Ed25519)

cpp
#include <sodium.h>

void signing_example() {
    // Generate key pair (Ed25519 — Edwards-curve digital signature)
    uint8_t pk[crypto_sign_PUBLICKEYBYTES];   // 32 bytes
    uint8_t sk[crypto_sign_SECRETKEYBYTES];   // 64 bytes
    crypto_sign_keypair(pk, sk);

    // Sign a message
    std::string message = "pay Alice $100";
    std::vector<uint8_t> signed_msg(crypto_sign_BYTES + message.size());
    unsigned long long signed_len = 0;

    crypto_sign(signed_msg.data(), &signed_len,
        reinterpret_cast<const uint8_t*>(message.data()), message.size(), sk);

    // Verify (only public key needed)
    std::vector<uint8_t> verified(message.size());
    unsigned long long verified_len = 0;
    if (crypto_sign_open(verified.data(), &verified_len,
                         signed_msg.data(), signed_len, pk) != 0)
        throw std::runtime_error("signature invalid or tampered");

    // For large messages, use detached signature instead:
    uint8_t sig[crypto_sign_BYTES];
    crypto_sign_detached(sig, nullptr,
        reinterpret_cast<const uint8_t*>(message.data()), message.size(), sk);
    // Verify: crypto_sign_verify_detached(sig, msg_data, msg_len, pk)
}

Key exchange (X25519 / Diffie-Hellman)

cpp
void key_exchange_example() {
    // Alice generates ephemeral key pair
    uint8_t alice_pk[crypto_kx_PUBLICKEYBYTES];
    uint8_t alice_sk[crypto_kx_SECRETKEYBYTES];
    crypto_kx_keypair(alice_pk, alice_sk);

    // Bob generates ephemeral key pair
    uint8_t bob_pk[crypto_kx_PUBLICKEYBYTES];
    uint8_t bob_sk[crypto_kx_SECRETKEYBYTES];
    crypto_kx_keypair(bob_pk, bob_sk);

    // Exchange public keys (sent over the network)

    // Alice derives shared session keys from Bob's public key
    uint8_t alice_rx[crypto_kx_SESSIONKEYBYTES];  // key for receiving
    uint8_t alice_tx[crypto_kx_SESSIONKEYBYTES];  // key for sending
    crypto_kx_client_session_keys(alice_rx, alice_tx, alice_pk, alice_sk, bob_pk);

    // Bob derives matching session keys from Alice's public key
    uint8_t bob_rx[crypto_kx_SESSIONKEYBYTES];
    uint8_t bob_tx[crypto_kx_SESSIONKEYBYTES];
    crypto_kx_server_session_keys(bob_rx, bob_tx, bob_pk, bob_sk, alice_pk);

    // alice_tx == bob_rx  (Alice sends → Bob receives with this key)
    // alice_rx == bob_tx  (Bob sends → Alice receives with this key)
    // Both now have symmetric keys without ever sending the key over the network
}

RSA via OpenSSL (legacy — prefer Ed25519/X25519)

cpp
#include <openssl/evp.h>
#include <openssl/rsa.h>
#include <openssl/pem.h>

// Generate RSA-4096 key pair
EVP_PKEY* generate_rsa_key() {
    EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, nullptr);
    EVP_PKEY_keygen_init(ctx);
    EVP_PKEY_CTX_set_rsa_keygen_bits(ctx, 4096);
    EVP_PKEY* pkey = nullptr;
    EVP_PKEY_keygen(ctx, &pkey);
    EVP_PKEY_CTX_free(ctx);
    return pkey;  // free with EVP_PKEY_free
}

// Write PEM to file
void write_private_key(EVP_PKEY* pkey, const char* path) {
    FILE* f = fopen(path, "wb");
    PEM_write_PrivateKey(f, pkey, nullptr, nullptr, 0, nullptr, nullptr);
    fclose(f);
}

CMake Setup

cmake
find_package(OpenSSL REQUIRED)
target_link_libraries(myapp PRIVATE OpenSSL::SSL OpenSSL::Crypto)

# libsodium (via pkg-config or manual)
find_package(PkgConfig REQUIRED)
pkg_check_modules(LIBSODIUM REQUIRED libsodium)
target_link_libraries(myapp PRIVATE ${LIBSODIUM_LIBRARIES})
target_include_directories(myapp PRIVATE ${LIBSODIUM_INCLUDE_DIRS})

Checklist

  • Never implement your own crypto primitives
  • Use AES-256-GCM or ChaCha20-Poly1305 for symmetric encryption
  • Use ECDH (X25519) or RSA-OAEP for key exchange
  • Never reuse nonces/IVs with the same key
  • Use constant-time comparison for MAC verification (CRYPTO_memcmp)
  • Hash passwords with Argon2id/bcrypt/scrypt — never SHA-256 alone
  • Generate keys/nonces with RAND_bytes or randombytes_buf

Asymmetric key operations — ECDSA and RSA signing with OpenSSL

ECDSA over P-256 or Ed25519 produces compact signatures (64 bytes) with strong security and is preferred over RSA for new systems. OpenSSL's EVP layer abstracts over all key types so the sign/verify code is nearly identical regardless of algorithm.

cpp
#include <openssl/evp.h>
#include <openssl/ec.h>
#include <openssl/err.h>

// Generate an EC key (P-256)
EVP_PKEY* generate_ec_p256() {
    EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_EC, nullptr);
    EVP_PKEY_keygen_init(ctx);
    EVP_PKEY_CTX_set_ec_paramgen_curve_nid(ctx, NID_X9_62_prime256v1);
    EVP_PKEY* pkey = nullptr;
    EVP_PKEY_keygen(ctx, &pkey);
    EVP_PKEY_CTX_free(ctx);
    return pkey;  // free with EVP_PKEY_free
}

// Sign with ECDSA-SHA256
std::vector<uint8_t> ecdsa_sign(EVP_PKEY* pkey,
                                std::span<const uint8_t> message) {
    EVP_MD_CTX* ctx = EVP_MD_CTX_new();
    EVP_DigestSignInit(ctx, nullptr, EVP_sha256(), nullptr, pkey);
    EVP_DigestSignUpdate(ctx, message.data(), message.size());

    size_t sig_len = 0;
    EVP_DigestSignFinal(ctx, nullptr, &sig_len);  // get size first
    std::vector<uint8_t> sig(sig_len);
    EVP_DigestSignFinal(ctx, sig.data(), &sig_len);
    sig.resize(sig_len);

    EVP_MD_CTX_free(ctx);
    return sig;
}

// Verify ECDSA signature
bool ecdsa_verify(EVP_PKEY* pkey,
                  std::span<const uint8_t> message,
                  std::span<const uint8_t> sig) {
    EVP_MD_CTX* ctx = EVP_MD_CTX_new();
    EVP_DigestVerifyInit(ctx, nullptr, EVP_sha256(), nullptr, pkey);
    EVP_DigestVerifyUpdate(ctx, message.data(), message.size());
    int rc = EVP_DigestVerifyFinal(ctx, sig.data(), sig.size());
    EVP_MD_CTX_free(ctx);
    return rc == 1;
}
AlgorithmKey sizeSig sizeSecurityNotes
RSA-2048256 B256 B112-bitLegacy; large keys
RSA-4096512 B512 B140-bitSlow signing
ECDSA P-25632 B~72 B128-bitCommon in TLS/JWT
Ed2551932 B64 B128-bitFastest; deterministic

Hashing and MACs — choosing the right primitive

SHA-256 is the workhorse for integrity; BLAKE2b beats it in speed on software with equivalent security; HMAC-SHA256 adds a secret key for authentication. Never use MD5 or SHA-1 for security purposes.

cpp
#include <openssl/evp.h>

// Generic digest helper (SHA-256, SHA-512, BLAKE2b-512, etc.)
std::vector<uint8_t> digest(const EVP_MD* md, std::string_view data) {
    EVP_MD_CTX* ctx = EVP_MD_CTX_new();
    unsigned int len = 0;
    std::vector<uint8_t> hash(EVP_MD_size(md));

    EVP_DigestInit_ex(ctx, md, nullptr);
    EVP_DigestUpdate(ctx, data.data(), data.size());
    EVP_DigestFinal_ex(ctx, hash.data(), &len);
    EVP_MD_CTX_free(ctx);
    hash.resize(len);
    return hash;
}

// Usage
auto sha256  = digest(EVP_sha256(),      "hello");
auto sha512  = digest(EVP_sha512(),      "hello");
auto blake2b = digest(EVP_blake2b512(),  "hello");  // OpenSSL 1.1+

// BLAKE2 directly via libsodium (simpler API)
#include <sodium.h>
uint8_t out[crypto_generichash_BYTES];  // 32 bytes default
crypto_generichash(out, sizeof(out),
    reinterpret_cast<const uint8_t*>("hello"), 5,
    nullptr, 0);  // no key — pure hash

// Keyed BLAKE2b (acts like an HMAC)
uint8_t key[crypto_generichash_KEYBYTES];
randombytes_buf(key, sizeof(key));
crypto_generichash(out, sizeof(out),
    reinterpret_cast<const uint8_t*>("data"), 4,
    key, sizeof(key));
PrimitivePurposeKeyed?Speed (software)
SHA-256Integrity, certificate fingerprintsNo~400 MB/s
SHA-512Integrity, longer outputNo~600 MB/s
BLAKE2bGeneral-purpose fast hashingOptional~1 GB/s+
HMAC-SHA256Message authentication codeYes (required)~350 MB/s
BLAKE2b keyedMAC (simpler than HMAC)Yes (optional)~1 GB/s+
Poly1305MAC paired with ChaCha20YesVery fast (SIMD)

Key derivation functions (KDF)

Passwords must never be stored as raw hashes — use a memory-hard KDF like Argon2id so that brute-force is expensive even with GPUs. Use PBKDF2 only when FIPS compliance mandates it; use scrypt or Argon2id otherwise.

cpp
#include <openssl/kdf.h>

// PBKDF2-HMAC-SHA256 via OpenSSL
std::vector<uint8_t> pbkdf2(
    std::string_view password,
    std::span<const uint8_t> salt,
    int iterations,
    int key_len)
{
    std::vector<uint8_t> key(key_len);
    PKCS5_PBKDF2_HMAC(
        password.data(), static_cast<int>(password.size()),
        salt.data(),     static_cast<int>(salt.size()),
        iterations,
        EVP_sha256(),
        key_len, key.data());
    return key;
}

// Argon2id via libsodium — recommended for password hashing
#include <sodium.h>

std::string argon2id_hash(std::string_view password) {
    char hashed[crypto_pwhash_STRBYTES];

    if (crypto_pwhash_str(
            hashed, password.data(), password.size(),
            crypto_pwhash_OPSLIMIT_SENSITIVE,   // higher = slower = more secure
            crypto_pwhash_MEMLIMIT_SENSITIVE)   // memory usage
        != 0)
        throw std::runtime_error("Argon2id: out of memory");

    return std::string(hashed);  // encoded string with salt+params embedded
}

bool argon2id_verify(std::string_view stored_hash, std::string_view password) {
    return crypto_pwhash_str_verify(
        stored_hash.data(),
        password.data(), password.size()) == 0;
}

// HKDF — derive multiple subkeys from one master key (not for passwords)
std::vector<uint8_t> hkdf_sha256(
    std::span<const uint8_t> ikm,    // input key material
    std::span<const uint8_t> salt,
    std::string_view info,
    size_t out_len)
{
    std::vector<uint8_t> out(out_len);
    EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_HKDF, nullptr);
    EVP_PKEY_derive_init(ctx);
    EVP_PKEY_CTX_set_hkdf_md(ctx, EVP_sha256());
    EVP_PKEY_CTX_set1_hkdf_salt(ctx, salt.data(), salt.size());
    EVP_PKEY_CTX_set1_hkdf_key(ctx, ikm.data(), ikm.size());
    EVP_PKEY_CTX_add1_hkdf_info(ctx,
        reinterpret_cast<const uint8_t*>(info.data()), info.size());
    EVP_PKEY_derive(ctx, out.data(), &out_len);
    EVP_PKEY_CTX_free(ctx);
    out.resize(out_len);
    return out;
}
KDFUse caseGPU resistanceFIPS
PBKDF2-SHA256FIPS-mandated environmentsLowYes
bcryptPassword storage (legacy)MediumNo
scryptPassword storageHigh (memory)No
Argon2idPassword storage (recommended)Very highNo
HKDFSubkey derivation from master keyN/A (not for passwords)Yes

TLS session handling with OpenSSL

Setting up a TLS client or server requires configuring an SSL_CTX with the right protocol version, certificate verification policy, and optional extensions like SNI and ALPN. Asio's ssl::stream wraps the raw TCP stream and delegates all TLS logic to OpenSSL.

cpp
#include <asio/ssl.hpp>

// TLS client with certificate verification
asio::awaitable<std::string> tls_get(std::string host, std::string target) {
    auto executor = co_await asio::this_coro::executor;

    // SSL context — use TLS_client_method for automatic protocol negotiation
    asio::ssl::context ctx(asio::ssl::context::tls_client);
    ctx.set_default_verify_paths();              // load system CA bundle
    ctx.set_verify_mode(asio::ssl::verify_peer); // enforce certificate validation

    // ALPN: advertise HTTP/2 and HTTP/1.1
    SSL_CTX_set_alpn_protos(ctx.native_handle(),
        reinterpret_cast<const uint8_t*>("\x02h2\x08http/1.1"), 11);

    asio::ssl::stream<asio::ip::tcp::socket> tls_stream(executor, ctx);

    // SNI: tell the server which hostname we're connecting to
    SSL_set_tlsext_host_name(tls_stream.native_handle(), host.c_str());
    tls_stream.set_verify_callback(
        asio::ssl::host_name_verification(host));  // verify CN/SAN matches host

    asio::ip::tcp::resolver resolver(executor);
    auto ep = co_await resolver.async_resolve(host, "443", asio::use_awaitable);
    co_await asio::async_connect(tls_stream.next_layer(), ep, asio::use_awaitable);
    co_await tls_stream.async_handshake(asio::ssl::stream_base::client, asio::use_awaitable);

    // Log negotiated protocol
    const uint8_t* proto; unsigned int proto_len;
    SSL_get0_alpn_selected(tls_stream.native_handle(), &proto, &proto_len);
    std::println("ALPN: {}", std::string_view(reinterpret_cast<const char*>(proto), proto_len));

    // Send HTTP/1.1 GET (or HTTP/2 frames if negotiated)
    std::string req = "GET " + target + " HTTP/1.1\r\nHost: " + host + "\r\n\r\n";
    co_await asio::async_write(tls_stream, asio::buffer(req), asio::use_awaitable);

    std::string response;
    co_await asio::async_read_until(tls_stream,
        asio::dynamic_buffer(response), "\r\n\r\n", asio::use_awaitable);
    co_return response;
}

// TLS server — load certificate and key
asio::ssl::context make_server_ctx(const std::string& cert_pem,
                                   const std::string& key_pem) {
    asio::ssl::context ctx(asio::ssl::context::tls_server);
    ctx.use_certificate_chain_file(cert_pem);
    ctx.use_private_key_file(key_pem, asio::ssl::context::pem);
    ctx.set_verify_mode(asio::ssl::verify_none);  // client cert not required

    // Enforce TLS 1.2 minimum
    SSL_CTX_set_min_proto_version(ctx.native_handle(), TLS1_2_VERSION);

    // Cipher list (TLS 1.2 and below)
    SSL_CTX_set_cipher_list(ctx.native_handle(),
        "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384");

    return ctx;
}

Constant-time code and timing attacks

Branching or short-circuiting on secret data allows an attacker to infer the value via precise timing measurements — even over a network. Use library-provided constant-time primitives instead of memcmp or manual loops.

cpp
#include <sodium.h>
#include <openssl/crypto.h>

// WRONG — early exit leaks information about where first mismatch is
bool bad_compare(const uint8_t* a, const uint8_t* b, size_t n) {
    for (size_t i = 0; i < n; ++i)
        if (a[i] != b[i]) return false;  // timing varies with mismatch position
    return true;
}

// WRONG — std::memcmp may be optimized to short-circuit
bool also_bad(const uint8_t* a, const uint8_t* b, size_t n) {
    return std::memcmp(a, b, n) == 0;
}

// CORRECT — libsodium: constant time regardless of content
bool sodium_equal(const uint8_t* a, const uint8_t* b, size_t n) {
    return sodium_memcmp(a, b, n) == 0;
}

// CORRECT — OpenSSL CRYPTO_memcmp
bool openssl_equal(const uint8_t* a, const uint8_t* b, size_t n) {
    return CRYPTO_memcmp(a, b, n) == 0;
}

// Constant-time select (no branch on secret boolean)
// Returns a if cond == 1, b if cond == 0
uint8_t ct_select(uint8_t cond, uint8_t a, uint8_t b) {
    // cond must be exactly 0 or 1
    return (a & (-(uint8_t)cond)) | (b & (~(-(uint8_t)cond)));
}

// Constant-time zero check
bool ct_is_zero(std::span<const uint8_t> buf) {
    uint8_t acc = 0;
    for (uint8_t b : buf) acc |= b;
    return acc == 0;
}

Vulnerabilities that constant-time code prevents:

AttackVectorFix
MAC forgery (HMAC timing)Compare expected vs received MAC byte-by-byteCRYPTO_memcmp
Password oracleHash comparison exits early on mismatchsodium_memcmp
RSA private key leakageModular exponentiation timingOpenSSL blinding (automatic)
AES cache-timing (non-AES-NI)Table lookup timing varies by keyUse AES-NI hardware instruction

Cryptographically secure random number generation

/dev/urandom and the getrandom syscall are the OS-level CSPRNGs; both are seeded from hardware entropy at boot. Never use rand(), srand(), or std::mt19937 for cryptographic purposes.

cpp
#include <openssl/rand.h>
#include <sys/random.h>   // getrandom syscall (Linux 3.17+)
#include <sodium.h>

// OpenSSL RAND_bytes — seeds from OS CSPRNG automatically
std::vector<uint8_t> csprng_openssl(size_t n) {
    std::vector<uint8_t> buf(n);
    if (RAND_bytes(buf.data(), static_cast<int>(n)) != 1) {
        unsigned long err = ERR_get_error();
        throw std::runtime_error(ERR_error_string(err, nullptr));
    }
    return buf;
}

// getrandom syscall — bypasses /dev/urandom file descriptor
std::vector<uint8_t> csprng_getrandom(size_t n) {
    std::vector<uint8_t> buf(n);
    ssize_t got = getrandom(buf.data(), n, 0);  // flags=0: block until seeded
    if (got < 0 || static_cast<size_t>(got) != n)
        throw std::runtime_error("getrandom failed");
    return buf;
}

// libsodium randombytes — portable across Linux/macOS/Windows
std::vector<uint8_t> csprng_sodium(size_t n) {
    std::vector<uint8_t> buf(n);
    randombytes_buf(buf.data(), n);  // never fails; aborts on catastrophic failure
    return buf;
}

// Generating a 256-bit AES key + 96-bit nonce for AES-GCM
auto key   = csprng_sodium(32);  // 256 bits
auto nonce = csprng_sodium(12);  // 96 bits — never reuse with the same key!
SourcePortableBlocks until seededNotes
RAND_bytes (OpenSSL)YesYes (OpenSSL 3)Falls back to OS CSPRNG
getrandom(0)Linux 3.17+YesPreferred for Linux servers
/dev/urandomPOSIXNo (may return weak entropy early boot)Older; fd overhead
randombytes_buf (libsodium)YesYesWraps best available source
std::mt19937YesN/ANOT cryptographically secure

Certificate management and X.509 parsing

X.509 certificates encode the public key, identity (CN/SAN), validity period, and the issuing CA's signature — parsing them with OpenSSL lets you inspect or validate a chain before using it. Certificate pinning and OCSP stapling are two common hardening techniques applied on top of standard chain validation.

cpp
#include <openssl/x509.h>
#include <openssl/x509v3.h>
#include <openssl/pem.h>

// Parse a PEM certificate from a file
X509* load_cert_pem(const char* path) {
    FILE* f = fopen(path, "r");
    X509* cert = PEM_read_X509(f, nullptr, nullptr, nullptr);
    fclose(f);
    return cert;  // free with X509_free
}

// Extract Subject Alternative Names (used for HTTPS hostname verification)
std::vector<std::string> get_sans(X509* cert) {
    std::vector<std::string> names;
    GENERAL_NAMES* sans =
        static_cast<GENERAL_NAMES*>(X509_get_ext_d2i(cert, NID_subject_alt_name,
                                                      nullptr, nullptr));
    if (!sans) return names;

    for (int i = 0; i < sk_GENERAL_NAME_num(sans); ++i) {
        GENERAL_NAME* name = sk_GENERAL_NAME_value(sans, i);
        if (name->type == GEN_DNS) {
            const char* dns =
                reinterpret_cast<const char*>(ASN1_STRING_get0_data(name->d.dNSName));
            names.emplace_back(dns, ASN1_STRING_length(name->d.dNSName));
        }
    }
    GENERAL_NAMES_free(sans);
    return names;
}

// Print certificate details
void print_cert_info(X509* cert) {
    // Subject
    char subject[256];
    X509_NAME_oneline(X509_get_subject_name(cert), subject, sizeof(subject));
    std::println("Subject: {}", subject);

    // Issuer
    char issuer[256];
    X509_NAME_oneline(X509_get_issuer_name(cert), issuer, sizeof(issuer));
    std::println("Issuer:  {}", issuer);

    // Validity
    ASN1_TIME* not_before = X509_get_notBefore(cert);
    ASN1_TIME* not_after  = X509_get_notAfter(cert);
    // (convert with ASN1_TIME_print or ASN1_TIME_to_tm)

    // SANs
    for (auto& san : get_sans(cert))
        std::println("SAN: {}", san);
}

// Validate a chain against a trusted CA bundle
bool validate_chain(X509* leaf,
                    const std::vector<X509*>& intermediates,
                    const char* ca_bundle_pem)
{
    X509_STORE* store = X509_STORE_new();
    X509_STORE_load_locations(store, ca_bundle_pem, nullptr);

    X509_STORE_CTX* ctx = X509_STORE_CTX_new();
    STACK_OF(X509)* chain = sk_X509_new_null();
    for (X509* c : intermediates) sk_X509_push(chain, c);

    X509_STORE_CTX_init(ctx, store, leaf, chain);
    int ok = X509_verify_cert(ctx);

    if (!ok)
        std::println("Cert error: {}",
            X509_verify_cert_error_string(X509_STORE_CTX_get_error(ctx)));

    X509_STORE_CTX_free(ctx);
    sk_X509_free(chain);
    X509_STORE_free(store);
    return ok == 1;
}

Certificate pinning — reject connections whose leaf certificate does not match an expected fingerprint:

cpp
bool pin_matches(X509* cert, std::span<const uint8_t> expected_sha256) {
    uint8_t fingerprint[32];
    unsigned int len = sizeof(fingerprint);

    // Hash the DER encoding of the certificate (same as browser pins)
    unsigned char* der = nullptr;
    int der_len = i2d_X509(cert, &der);
    EVP_Digest(der, der_len, fingerprint, &len, EVP_sha256(), nullptr);
    OPENSSL_free(der);

    return CRYPTO_memcmp(fingerprint, expected_sha256.data(), 32) == 0;
}