Domain: Identity & Access Systems
π Used in: APIs, SaaS platforms, fintech, internal tools, developer platforms, enterprise SSO, mobile backends
β One of the most practical cryptography domains: combines password hashing, token signing, authenticated sessions, randomness, and secure protocol design.
Identity systems answer two fundamental questions:
- Who are you? β authentication
- What are you allowed to do? β authorization
Modern identity systems are not built from a single primitive.
They are built from a composition of primitives:
- Argon2 protects passwords at rest
- HMAC protects tokens against tampering
- Ed25519 enables public verification
- AEAD protects confidential session state
- Randomness prevents replay and prediction
- Hashing enables safe logging and correlation
This chapter shows how those primitives are applied in a real domain.
Not as isolated theory. As a working security architecture.
Threat Model
Identity systems are constantly attacked.
Attackers try to:
- steal password databases
- brute-force leaked hashes
- forge tokens
- escalate privileges
- replay login challenges
- tamper with session state
- abuse weak randomness
- exploit overexposed long-term secrets
Identity Systems as Composition of Primitives
Cryptography is not used once. It appears at every layer of the identity system.
| Component | Primitive | Security Property | Why it matters |
|---|---|---|---|
| password storage | Argon2 | brute-force resistance | database leaks happen |
| internal stateless token | HMAC | integrity + authenticity | prevents privilege escalation |
| distributed token verification | Ed25519 | public verifiability | proves identity |
| session protection | AEAD | confidentiality + integrity | protects sensitive state |
| login challenges | CSPRNG | unpredictability | prevents reuse of captured messages |
| safe observability | BLAKE3 | non-reversible fingerprinting | limits blast radius1 |
| session lifecycle control | expiration policy | expiration enforcement | prevents long-lived compromise |
Password Storage β Argon2
Passwords must never be stored directly.
If a database leaks and the server stored plaintext passwords, every account is immediately compromised. Even plain hashing is not enough.
General-purpose hashes are designed to be fast. Password hashing must be deliberately expensive.
Argon2 is designed for this exact purpose.
password + salt β Argon2 β password hash
This protects users even when the database is stolen.
The server stores only the derived hash, never the password itself.
π§ͺ Minimal Rust Example: password hashing and verification (source code)
Crates used: argon2 , rand_core
use argon2::{
Argon2,
password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
};
use rand_core::OsRng;
use rsa::rand_core;
fn hash_password(password: &str) -> Result<String, Box<dyn std::error::Error>> {
let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default();
let password_hash = argon2
.hash_password(password.as_bytes(), &salt)?
.to_string();
Ok(password_hash)
}
fn verify_password(password: &str, stored_hash: &str) -> Result<bool, Box<dyn std::error::Error>> {
let parsed_hash = PasswordHash::new(stored_hash)?;
let argon2 = Argon2::default();
Ok(argon2
.verify_password(password.as_bytes(), &parsed_hash)
.is_ok())
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let password = "correct horse battery staple";
let stored_hash = hash_password(password)?;
println!("Stored hash:\n{stored_hash}\n");
let valid = verify_password(password, &stored_hash)?;
println!("Valid password: {valid}");
let invalid = verify_password("wrong password", &stored_hash)?;
println!("Wrong password accepted: {invalid}");
Ok(())
}
Output:
Stored hash:
$argon2id$v=19$m=19456,t=2,p=1$G1/J1ZCmovy3XioeKSPC1Q$RRoz1mRaShxHqnvpq3JiGR0xuScwdoO6MOcWkUw0cIU
Valid password: true
Wrong password accepted: false
π’ Conclusion
Password hashing solves one specific problem: if the database leaks, raw passwords are not immediately exposed.
Token Integrity β HMAC
After authentication, the server often issues a token.
That token may contain claims such as:
- user identifier
- role
- expiration time
- scopes
If attackers can modify the token, they can escalate privileges. So the token must be protected against tampering.
A simple and practical way to do that is HMAC.
payload + secret key β MAC
When the token comes back, the server recomputes the MAC and checks that the token was not modified.
π§ͺ Minimal Rust Example: HMAC-signed token (source code)
Crates used: hmac, sha2, base64
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
use hmac::{Hmac, Mac};
use sha2::Sha256;
type HmacSha256 = Hmac<Sha256>;
fn sign_token(payload: &str, secret: &[u8]) -> Result<String, Box<dyn std::error::Error>> {
let mut mac = HmacSha256::new_from_slice(secret)?;
mac.update(payload.as_bytes());
let tag = mac.finalize().into_bytes();
let payload_b64 = URL_SAFE_NO_PAD.encode(payload.as_bytes());
let tag_b64 = URL_SAFE_NO_PAD.encode(tag);
Ok(format!("{payload_b64}.{tag_b64}"))
}
fn verify_token(token: &str, secret: &[u8]) -> Result<Option<String>, Box<dyn std::error::Error>> {
let Some((payload_b64, tag_b64)) = token.split_once('.') else {
return Ok(None);
};
let payload = match URL_SAFE_NO_PAD.decode(payload_b64) {
Ok(bytes) => bytes,
Err(_) => return Ok(None),
};
let tag = match URL_SAFE_NO_PAD.decode(tag_b64) {
Ok(bytes) => bytes,
Err(_) => return Ok(None),
};
let mut mac = HmacSha256::new_from_slice(secret)?;
mac.update(&payload);
if mac.verify_slice(&tag).is_ok() {
Ok(Some(String::from_utf8(payload)?))
} else {
Ok(None)
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let secret = b"server-secret-key";
let payload = r#"{"sub":"alice","role":"user","exp":1735689600}"#;
let token = sign_token(payload, secret)?;
println!("Token:\n{token}\n");
let verified = verify_token(&token, secret)?;
println!("Verified payload: {verified:?}");
let tampered_token = token.replace("\"user\"", "\"admin\"");
let verified_tampered = verify_token(&tampered_token, secret)?;
println!("Tampered token accepted: {verified_tampered:?}");
Ok(())
}
Output:
Token:
eyJzdWIiOiJhbGljZSIsInJvbGUiOiJ1c2VyIiwiZXhwIjoxNzM1Njg5NjAwfQ.Atg477etFsR_F47QcJz1NINk6xLQCi3PEfs_nAectSQ
Verified payload: Some("{\"sub\":\"alice\",\"role\":\"user\",\"exp\":1735689600}")
Tampered token accepted: None
π’ Conclusion
HMAC ensures that a token cannot be modified without detection. This is enough for many internal systems where all verifiers can share the same secret.
Public Verification β Ed25519 Signatures
HMAC works well when one server or a tightly controlled backend verifies everything.
But some identity systems need broader verification:
- API gateways
- microservices
- third-party systems
- federated identity flows
- passwordless authentication
In those systems, sharing one secret with every verifier is a bad idea.
Digital signatures solve this.
payload β Sign(private_key)
payload + signature β Verify(public_key)
The signer keeps the private key secret.
Everyone else can verify using the public key.
π§ͺ Minimal Rust Example: Ed25519 signing and verification (source code)
Crates used: ed25519-dalek, rand_core
use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
use rand_core::OsRng;
fn sign_claims(message: &[u8]) -> (SigningKey, VerifyingKey, Signature) {
let signing_key = SigningKey::generate(&mut OsRng);
let verifying_key = signing_key.verifying_key();
let signature = signing_key.sign(message);
(signing_key, verifying_key, signature)
}
fn verify_claims(message: &[u8], verifying_key: &VerifyingKey, signature: &Signature) -> bool {
verifying_key.verify(message, signature).is_ok()
}
fn main() {
let claims = br#"{"sub":"alice","role":"admin","exp":1735689600}"#;
let (_signing_key, verifying_key, signature) = sign_claims(claims);
let valid = verify_claims(claims, &verifying_key, &signature);
println!("Valid signature: {valid}");
let tampered_claims = br#"{"sub":"alice","role":"super_admin","exp":1735689600}"#;
let tampered_valid = verify_claims(tampered_claims, &verifying_key, &signature);
println!("Tampered claims accepted: {tampered_valid}");
}
Output:
Valid signature: true
Tampered claims accepted: false
π’ Conclusion
Signatures are the right primitive when many parties need to verify identity assertions without sharing signing power.
Confidential Sessions β AEAD
Some identity-related data must not only be authenticated.
It must also remain secret.
Examples:
- encrypted cookies
- delegated session state
- recovery flow metadata
- internal authentication context
- CSRF-related blobs2
This is where AEAD is the right tool.
AEAD provides, in one construction:
- confidentiality
- integrity
- authenticity
π§ͺ Minimal Rust Example: encrypting session state (source code)
Crates used: chacha20poly1305, rand
use chacha20poly1305::{
aead::{Aead, KeyInit},
ChaCha20Poly1305, Key, Nonce,
};
use rand::RngCore;
fn encrypt_session(
plaintext: &[u8],
key_bytes: [u8; 32],
) -> Result<(Vec<u8>, [u8; 12]), Box<dyn std::error::Error>> {
let cipher = ChaCha20Poly1305::new(Key::from_slice(&key_bytes));
let mut nonce_bytes = [0u8; 12];
rand::thread_rng().fill_bytes(&mut nonce_bytes);
let nonce = Nonce::from_slice(&nonce_bytes);
let ciphertext = cipher
.encrypt(nonce, plaintext)
.map_err(|_| chacha20poly1305::Error)
.unwrap();
Ok((ciphertext, nonce_bytes))
}
fn decrypt_session(
ciphertext: &[u8],
nonce_bytes: [u8; 12],
key_bytes: [u8; 32],
) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let cipher = ChaCha20Poly1305::new(Key::from_slice(&key_bytes));
let nonce = Nonce::from_slice(&nonce_bytes);
let plaintext = cipher
.decrypt(nonce, ciphertext)
.map_err(|_| chacha20poly1305::Error)
.unwrap();
Ok(plaintext)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let key = [7u8; 32];
let session_data = br#"{"sub":"alice","csrf":"abc123","mfa":"pending"}"#;
let (ciphertext, nonce) = encrypt_session(session_data, key)?;
println!("Ciphertext length: {}", ciphertext.len());
let plaintext = decrypt_session(&ciphertext, nonce, key)?;
println!("Decrypted session: {}", String::from_utf8(plaintext)?);
Ok(())
}
Output:
Ciphertext length: 63
Decrypted session: {"sub":"alice","csrf":"abc123","mfa":"pending"}
π’ Conclusion
AEAD protects session state that must remain hidden and tamper-proof.
-
Blast radius: the scope of impact when something fails in a system. It describes how much of the system is affected by a bug, outage, security breach, or bad deployment. Goal in engineering: keep the blast radius as small as possible so failures stay contained. More β©
-
CSRF (Cross-Site Request Forgery): an attack where a malicious website tricks a userβs browser into sending an unwanted request to another site where the user is already authenticated. More β©