The friction is the feature
You're building a login system and need to hash passwords. You search for a crypto library and find ring. It's the most recommended crate. You add it to Cargo.toml. You write the code. The compiler rejects you because you passed a String instead of bytes. You fix that. It compiles. You run it. It works.
Then you try to disable SHA-512 to reduce binary size. You can't. ring has no feature flags. You try to find a trait that lets you swap algorithms dynamically. There isn't one. ring feels rigid, opinionated, and sometimes hostile.
That hostility is intentional. ring is designed to make it impossible to use cryptography incorrectly. Most crypto libraries let you pass raw bytes everywhere and hope you don't mix up a key with a nonce or a hash with a signature. ring uses the type system to enforce correctness. The friction you feel is the safety net catching you before you ship a vulnerability.
Think of ring as a certified hardware security module bolted to your desk. You can't just shove data into it. The module has specific slots for specific keys. It checks your paperwork. It refuses to run if the room isn't secure. The API forces you to be explicit about what you are doing. You pay for that safety with a steeper learning curve and a larger binary. The trade-off is worth it for anything security-critical.
Your first hash
Here is the minimal setup. You add ring as a dependency and compute a SHA-256 hash.
[dependencies]
ring = "0.17"
use ring::digest::{digest, SHA256};
fn main() {
// ring works on raw bytes. It does not care about UTF-8 or String semantics.
let message = b"Secret handshake";
// digest() takes a reference to an algorithm constant and the data.
// It returns a Digest struct, not a Vec or String.
let result = digest(&SHA256, message);
// Convert the Digest to bytes for display.
// The {:x} formatter prints hex.
println!("{:x}", result);
}
The digest function returns a Digest struct. This is not a Vec<u8>. It is a wrapper that knows its own length and algorithm. This prevents you from treating a hash as raw bytes without thinking about what it represents. To get the bytes out, you use .as_ref() or the Display trait.
Don't fight the Digest type. It's protecting you from treating hashes like strings.
How the types protect you
ring uses a pattern called "algorithm constants." You see types like SHA256, SHA512, HMAC_SHA256. These are zero-sized types. You never instantiate them. You just reference the global constant with &SHA256.
This design encodes the algorithm parameters into the type system. A function that takes &SHA256 cannot accidentally receive &SHA512. The compiler catches the mismatch. This eliminates entire classes of bugs where you configure the wrong algorithm at runtime.
Convention aside: You will see &SHA256 everywhere. Never write SHA256 without the ampersand unless the API explicitly asks for the value. The constants are references to static instances. Passing by reference is the standard.
ring also wraps keys in dedicated types. You cannot pass a raw byte slice as a key. You must construct a Key type. This ensures you never use a key where a message is expected, or vice versa. The types act as labels that the compiler enforces.
Realistic flow: Signing and verifying
Hashing is simple. Signatures are where ring shines. Here is how you sign a message with HMAC-SHA256 and verify it. This is the pattern you'll use for API authentication or message integrity.
use ring::hmac::{Key, Signature, HMAC_SHA256};
fn create_and_verify() {
// The secret key. In real code, this comes from a secure store.
let key_bytes = b"super-secret-key-do-not-share";
// Wrap the raw bytes in a Key type.
// This binds the bytes to the HMAC_SHA256 algorithm.
// You cannot use this Key with a different algorithm.
let key = Key::from(&HMAC_SHA256, key_bytes);
let message = b"Transfer $100 to Alice";
// Sign returns a Signature struct.
// The Signature type prevents you from confusing signatures with other data.
let signature = ring::hmac::sign(&key, message);
// To use the signature externally, extract the bytes.
let sig_bytes = signature.as_ref();
// Verify the signature.
// verify() returns Ok(()) on success, Err on failure.
// It uses constant-time comparison to prevent timing attacks.
let verification_result = ring::hmac::verify(&key, message, sig_bytes);
assert!(verification_result.is_ok());
// Tamper with the message.
let tampered_message = b"Transfer $1000 to Alice";
// Verification fails.
let tampered_result = ring::hmac::verify(&key, tampered_message, sig_bytes);
assert!(tampered_result.is_err());
}
The Key::from function is critical. It takes the algorithm and the bytes. If you pass the wrong length for the algorithm, ring may panic or return an error depending on the context. This check happens at key construction time, not at signing time. Fail fast.
The verify function is also important. It does not just check equality. It uses a constant-time comparison. If you compare signatures with ==, an attacker can measure the time it takes to fail and guess the signature byte by byte. ring handles this for you.
Wrap your keys. A raw byte slice is a liability; a Key type is a contract.
Pitfalls and compiler errors
You will hit compiler errors when you start with ring. The most common is E0308 (mismatched types). This happens when you pass a String or &str to a function that expects &[u8].
use ring::digest::{digest, SHA256};
fn bad_example() {
let message = String::from("Hello");
// Error[E0308]: mismatched types
// expected `&[u8]`, found `String`
let _hash = digest(&SHA256, message);
}
The fix is explicit. Call .as_bytes() on the string.
let _hash = digest(&SHA256, message.as_bytes());
Another pitfall is expecting feature flags. ring does not support toggling algorithms. You cannot disable SHA-512 to save space. The crate is a monolith. This is a design choice. The maintainers believe that feature flags increase the audit surface. If you can toggle features, you might toggle off a safety check. ring forces you to audit the entire crate. The binary size is the cost of that guarantee.
ring also does not support legacy algorithms. There is no SHA-1. There is no MD5. There is no RC4. If you need these, ring will not help. The maintainers deliberately omit broken or deprecated ciphers. You have to use a different crate for legacy support.
If ring doesn't have the algorithm, you probably shouldn't use the algorithm.
Thread safety and performance
ring is thread-safe. The high-level API functions like digest, sign, and verify can be called from multiple threads simultaneously. The algorithm constants are Sync. The Key types are Send and Sync. You can share a Key across threads without a mutex.
Under the hood, ring uses unsafe Rust and assembly code for performance. The core implementation is audited and tested extensively. The safe API wraps that unsafe core. You get the speed of hand-tuned assembly with the safety of Rust's type system.
ring is fast. It often matches or beats C libraries like OpenSSL. The assembly implementations are optimized for modern CPUs. The overhead of the safe API is negligible. You are not paying a performance penalty for safety.
When to use ring
Choosing a crypto crate depends on your needs. ring is not the only option. Use the right tool for the job.
Use ring when you need a battle-tested, audited cryptographic library that prioritizes correctness over flexibility. Use ring when you are building TLS, signatures, or hashing and want to avoid the complexity of openssl bindings. Use ring when you want to ensure your code cannot accidentally misuse cryptographic primitives due to type errors.
Reach for the sha2 or blake3 crates when you only need a single hash algorithm and want a generic, trait-based API that works with std::io::Read. Reach for rustls when you need a full TLS implementation; ring provides the primitives, rustls provides the protocol.
Avoid ring if you need obscure algorithms like SHA-1 or RC4; ring deliberately omits broken or legacy ciphers. Avoid ring if binary size is the absolute constraint and you can tolerate a less audited library; ring is heavy.
Pick the tool that matches your threat model, not just your feature list.