Blog

February 12, 2025 · Technical · 8 min read

Rust and memory safety in cryptographic libraries

Memory safety bugs have plagued cryptographic libraries for decades. How Rust rules out whole classes of them without giving up C-level performance, and how to migrate existing C code.

Memory safety bugs in cryptographic software

Heartbleed in OpenSSL, goto fail in GnuTLS, side-channel leaks in libgcrypt: cryptographic software has a long history of vulnerabilities caused by memory safety issues in C and C++.

The algorithms were sound. The implementations failed, because the languages make it too easy to write dangerous code.

How common these bugs are

Microsoft's security team analyzed decades of CVEs and found that 70% of security vulnerabilities are memory safety issues. Google's Chrome team reports similar numbers. For cryptographic libraries the percentage is even higher, because:

  1. Crypto code handles secrets: a buffer overflow leaks keys rather than only crashing.
  2. Performance pressure: optimizations often involve unsafe memory operations.
  3. Complex algorithms: more code means more opportunity for mistakes.

Memory safety CVEs in cryptographic libraries

LibraryCVETypeImpact
OpenSSLCVE-2014-0160Buffer over-readPrivate key disclosure
GnuTLSCVE-2014-3466Buffer overflowRemote code execution
libgcryptCVE-2017-7526Side-channelRSA key recovery
wolfSSLCVE-2020-12457Buffer overflowDenial of service
mbedTLSCVE-2021-44732Double freeRemote code execution

How Rust prevents these bugs

Rust's ownership system prevents memory safety bugs at compile time.

Buffer overflows

In C, nothing stops you from reading past array bounds:

// C: Compiles fine, causes Heartbleed-style vulnerability
void process_message(uint8_t *buf, size_t claimed_len) {
    uint8_t response[MAX_SIZE];
    // If claimed_len > actual length, we read arbitrary memory
    memcpy(response, buf, claimed_len);
    send_response(response, claimed_len);
}

In Rust, the compiler enforces bounds checking, so buffer overflows are impossible:

// Rust: Won't compile if bounds aren't checked
fn process_message(buf: &[u8]) -> Vec<u8> {
    // buf.len() is always accurate - can't lie about length
    let mut response = Vec::with_capacity(buf.len());
    response.extend_from_slice(buf);
    response
}

Use-after-free

C allows dangling pointers, and a single use-after-free can enable remote code execution:

// C: Use-after-free vulnerability
EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
// ... use ctx ...
EVP_CIPHER_CTX_free(ctx);
// ... later, accidentally ...
EVP_EncryptUpdate(ctx, out, &outlen, in, inlen);  // UAF!

Rust's ownership system prevents this entirely:

// Rust: Won't compile - ctx is moved/dropped
let ctx = CipherCtx::new();
// ... use ctx ...
drop(ctx);
// ctx.encrypt(&input)?;  // Compile error: use of moved value

Data races

Concurrent access to shared state causes subtle bugs:

// C: Data race on shared key state
void encrypt_parallel(key_state *state, uint8_t *data[], size_t count) {
    #pragma omp parallel for
    for (size_t i = 0; i < count; i++) {
        // Multiple threads modify state simultaneously - UB
        aes_encrypt(state, data[i]);
    }
}

Rust's type system enforces thread safety, so data races are impossible:

// Rust: Either shared (&T) or mutable (&mut T), never both
fn encrypt_parallel(key: &AesKey, data: &mut [&mut [u8]]) {
    data.par_iter_mut().for_each(|block| {
        // key is shared (immutable), blocks are exclusively accessed
        key.encrypt_block(block);
    });
}

Performance compared with C

A common misconception is that safety costs performance. Rust proves otherwise.

Zero-cost abstractions

Rust's abstractions compile away to the same machine code as hand-written C:

// High-level Rust
pub fn xor_blocks(a: &mut [u8; 16], b: &[u8; 16]) {
    for i in 0..16 {
        a[i] ^= b[i];
    }
}

// Compiles to the same assembly as:
// void xor_blocks(uint8_t *a, const uint8_t *b) {
//     for (int i = 0; i < 16; i++) a[i] ^= b[i];
// }

Benchmark: OpenCrypt vs C libraries

OperationOpenCrypt (Rust)OpenSSL (C)libsodium (C)
AES-256-GCM 1KB0.82 μs0.78 μs0.85 μs
SHA-256 1KB1.21 μs1.18 μs1.25 μs
X2551948.3 μs45.9 μs47.1 μs
Ed25519 Sign52.7 μs50.2 μs51.8 μs

Rust matches or exceeds C performance while eliminating memory safety bugs.

Rust cryptography libraries

The Rust community has built a solid cryptography ecosystem.

RustCrypto

A collection of pure-Rust cryptographic primitives:

  • AES, ChaCha20, Salsa20 ciphers
  • SHA-2, SHA-3, BLAKE3 hashes
  • RSA, ECDSA, Ed25519 signatures
  • Well-audited, constant-time implementations

ring

A focused library that uses BoringSSL's verified assembly:

  • Production-grade TLS primitives
  • Used by major projects like Cloudflare
  • Hybrid Rust/assembly approach

OpenCrypt

Our high-performance library, with post-quantum support:

  • Pure Rust, zero unsafe code
  • ML-DSA, ML-KEM implementations
  • Formally verified core operations

Writing secure crypto code in Rust

Even in Rust, cryptographic code needs care.

Constant-time comparison

Use the subtle crate for timing-safe comparisons:

use subtle::{ConstantTimeEq, Choice};

fn verify_mac(computed: &[u8; 32], received: &[u8; 32]) -> bool {
    // Constant-time comparison - no early exit
    computed.ct_eq(received).into()
}

Zeroizing secrets

Zeroize secrets when done:

use zeroize::Zeroize;

struct SecretKey {
    bytes: [u8; 32],
}

impl Drop for SecretKey {
    fn drop(&mut self) {
        self.bytes.zeroize();  // Overwrites memory on drop
    }
}

Branching on secrets

Be careful with conditional logic:

// BAD: Timing leak through branching
fn select_bad(condition: bool, a: u64, b: u64) -> u64 {
    if condition { a } else { b }  // Branch predictor leaks condition
}

// GOOD: Constant-time selection
fn select_good(condition: bool, a: u64, b: u64) -> u64 {
    let mask = -(condition as i64) as u64;
    (a & mask) | (b & !mask)
}

Migrating from C cryptographic libraries

Phase 1: new code in Rust

Write new cryptographic code in Rust and expose C-compatible interfaces with cbindgen:

#[no_mangle]
pub extern "C" fn opencrypt_aes_gcm_encrypt(
    key: *const u8,
    nonce: *const u8,
    plaintext: *const u8,
    plaintext_len: usize,
    ciphertext: *mut u8,
) -> i32 {
    // Safe Rust implementation called from C
}

Phase 2: gradual replacement

Replace C components one at a time, validating behavior with extensive testing.

Phase 3: pure Rust

Eventually remove C dependencies entirely, for full memory safety guarantees.

We think Rust should be the default for new cryptographic projects, and that migrating existing ones is worth the investment. Every line of OpenCrypt is written in safe Rust.

Questions about this post, or working on something similar? Book a meeting.