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)
#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):
#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:
#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
#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:
#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)
#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)
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)
#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
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_bytesorrandombytes_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.
#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;
}| Algorithm | Key size | Sig size | Security | Notes |
|---|---|---|---|---|
| RSA-2048 | 256 B | 256 B | 112-bit | Legacy; large keys |
| RSA-4096 | 512 B | 512 B | 140-bit | Slow signing |
| ECDSA P-256 | 32 B | ~72 B | 128-bit | Common in TLS/JWT |
| Ed25519 | 32 B | 64 B | 128-bit | Fastest; 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.
#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));| Primitive | Purpose | Keyed? | Speed (software) |
|---|---|---|---|
| SHA-256 | Integrity, certificate fingerprints | No | ~400 MB/s |
| SHA-512 | Integrity, longer output | No | ~600 MB/s |
| BLAKE2b | General-purpose fast hashing | Optional | ~1 GB/s+ |
| HMAC-SHA256 | Message authentication code | Yes (required) | ~350 MB/s |
| BLAKE2b keyed | MAC (simpler than HMAC) | Yes (optional) | ~1 GB/s+ |
| Poly1305 | MAC paired with ChaCha20 | Yes | Very 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.
#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;
}| KDF | Use case | GPU resistance | FIPS |
|---|---|---|---|
| PBKDF2-SHA256 | FIPS-mandated environments | Low | Yes |
| bcrypt | Password storage (legacy) | Medium | No |
| scrypt | Password storage | High (memory) | No |
| Argon2id | Password storage (recommended) | Very high | No |
| HKDF | Subkey derivation from master key | N/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.
#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.
#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:
| Attack | Vector | Fix |
|---|---|---|
| MAC forgery (HMAC timing) | Compare expected vs received MAC byte-by-byte | CRYPTO_memcmp |
| Password oracle | Hash comparison exits early on mismatch | sodium_memcmp |
| RSA private key leakage | Modular exponentiation timing | OpenSSL blinding (automatic) |
| AES cache-timing (non-AES-NI) | Table lookup timing varies by key | Use 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.
#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!| Source | Portable | Blocks until seeded | Notes |
|---|---|---|---|
RAND_bytes (OpenSSL) | Yes | Yes (OpenSSL 3) | Falls back to OS CSPRNG |
getrandom(0) | Linux 3.17+ | Yes | Preferred for Linux servers |
/dev/urandom | POSIX | No (may return weak entropy early boot) | Older; fd overhead |
randombytes_buf (libsodium) | Yes | Yes | Wraps best available source |
std::mt19937 | Yes | N/A | NOT 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.
#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:
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;
}