Security · cryptography in practice
Cryptographic security
A practical guide to how TSN's crypto module is built and reviewed — the post-quantum primitives in production, and a set of deliberately vulnerable teaching modules used to demonstrate the mistakes they guard against.
Overview
vulnerable-demo feature flag and must never be used in production.TSN uses algorithms resistant to quantum computers for everything that touches consensus and funds:
Signatures — ML-DSA-65 (FIPS 204)
use fips204::ml_dsa_65; let (pk, sk) = ml_dsa_65::try_keygen_with_rng(&mut OsRng)?; let signature = ml_dsa_65::try_sign_with_rng(&mut OsRng, &sk, &message, &context)?; let is_valid = ml_dsa_65::try_verify(&pk, &message, &signature, &context)?;
Why: resistant to quantum attacks, NIST-standardized, and — being a single fixed parameter set — the same code path signs every transaction, with no special-case fast/slow variants to keep in sync.
Hashing — Poseidon2 for ZK
use light_poseidon::Poseidon; let mut poseidon = Poseidon::new(); let hash = poseidon.hash(&inputs)?;
Why: optimized for zero-knowledge circuits, and — being an algebraic hash over Goldilocks/BN254 rather than a bitwise one — sidesteps the quantum-search speedups that apply to classical hash constructions.
Vulnerabilities demonstrated
The demonstration modules illustrate common cryptographic mistakes — each one paired with the fix TSN's production code actually uses.
# Build with the vulnerable modules (TESTING / EDUCATION ONLY) cargo build --features vulnerable-demo cargo test --features vulnerable-demo # Normal build (vulnerable modules EXCLUDED) cargo build cargo test
1. Timing attacks
Problem: a non-constant-time comparison leaks information through its execution time.
fn insecure_compare(a: &[u8], b: &[u8]) -> bool {
for i in 0..a.len() {
if a[i] != b[i] {
return false; // timing leak
}
}
true
}use subtle::ConstantTimeEq;
fn secure_compare(a: &[u8], b: &[u8]) -> bool {
a.ct_eq(b).into()
}Attack: measure the response time to recover a secret key one byte at a time.
2. Nonce reuse
Problem: reusing the same nonce reveals the XOR of the plaintexts: C1 ⊕ C2 = P1 ⊕ P2.
let nonce = [0u8; 12]; // always the same let ciphertext = cipher.encrypt(&nonce, plaintext)?;
let nonce = ChaCha20Poly1305::generate_nonce(&mut OsRng); let ciphertext = cipher.encrypt(&nonce, plaintext)?;
Attack: plaintext recovery through differential analysis.
3. Predictable RNG
Problem: a deterministic generator lets an attacker predict future keys.
let mut rng = StdRng::seed_from_u64(12345); // predictable sequence
let mut rng = OsRng; // cryptographic entropy
Attack: key prediction if the seed or internal state is ever exposed.
4. Padding oracle
Problem: a non-constant-time padding check leaks information through control flow.
fn remove_padding(data: &[u8]) -> Option<&[u8]> {
let pad_len = data[data.len() - 1] as usize;
if pad_len > 16 {
return None; // timing leak
}
// ...
}use subtle::{Choice, ConditionallySelectable};
fn secure_remove_padding(data: &[u8]) -> Option<&[u8]> {
// full constant-time check
}Attack: padding-oracle attack — decryption without ever recovering the key.
5. Naive KDF
Problem: derivation without salt or iteration count is vulnerable to rainbow tables.
fn naive_kdf(password: &[u8]) -> [u8; 32] {
Sha256::digest(password).into()
// no salt, no iterations
}use pbkdf2::pbkdf2_hmac; let mut key = [0u8; 32]; pbkdf2_hmac::(password, salt, 100_000, &mut key);
Attack: precomputed hash tables for common passwords (rainbow tables).
Protection by compilation
TSN uses compile-time guards to prevent the vulnerable modules from being used by accident:
#[cfg(not(feature = "vulnerable-demo"))]
compile_error!(
"ACCESS DENIED: vulnerable modules are disabled\n\
To enable them (TESTING / EDUCATION ONLY):\n\
cargo build --features vulnerable-demo"
);
This guarantees the vulnerable modules cannot be compiled into a production build by accident.
Security tests
Timing-attack benchmark
cargo test --features vulnerable-demo timing_attack_benchmark
Non-regression tests
# Verify the vulnerable modules stay locked out cargo build # must fail if vulnerable.rs is imported without the feature # Verify the feature flag itself works cargo build --features vulnerable-demo # must succeed
Crypto review checklist
Before merging any cryptographic code:
- Entropy — uses
OsRngfor production keys and nonces. - Constant-time — sensitive comparisons use
subtle::ConstantTimeEq. - AEAD — authenticated encryption (ChaCha20-Poly1305, AES-GCM).
- Post-quantum — ML-DSA-65 signatures, Poseidon2 hashing for ZK circuits.
- Secure KDF — PBKDF2 / scrypt / Argon2 with a random salt.
- Unique nonces — never reused.
- Tests — error paths and edge cases covered.
- Documentation — the reasoning behind each cryptographic choice is written down.