Keyboard shortcuts

Press ← or β†’ to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.

ComponentPrimitiveSecurity PropertyWhy it matters
password storageArgon2brute-force resistancedatabase leaks happen
internal stateless tokenHMACintegrity + authenticityprevents privilege escalation
distributed token verificationEd25519public verifiabilityproves identity
session protectionAEADconfidentiality + integrityprotects sensitive state
login challengesCSPRNGunpredictabilityprevents reuse of captured messages
safe observabilityBLAKE3non-reversible fingerprintinglimits blast radius1
session lifecycle controlexpiration policyexpiration enforcementprevents 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)

crate logo 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)

crate logo 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)

crate logo 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)

crate logo 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.


  1. 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 ↩

  2. 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 ↩