Bootcamp
A 60-minute crypto bootcamp: build a small token-signing + verification service that ships every modern best practice in one repo.
A 60-minute crypto bootcamp
EXAMPLE
# ===== Objectives =====
# 1. Hash passwords with argon2id
# 2. Sign + verify JWTs with RS256
# 3. Encrypt sensitive cookie values with AES-GCM
# 4. Sign webhook bodies with HMAC + timestamp
# 5. Constant-time compare for all secret comparisons
# 6. Add unit tests that prove the misuse path is closed
# ===== Stack =====
# Python + cryptography + argon2-cffi + pyjwt
# pip install cryptography argon2-cffi pyjwt fastapi uvicorn pytest
# ===== 0-10 min: password hashing =====
# auth.py
import argon2
_hasher = argon2.PasswordHasher(time_cost=3, memory_cost=64*1024, parallelism=4)
def hash_password(p: str) -> str:
return _hasher.hash(p)
def verify_password(stored: str, candidate: str) -> bool:
try:
_hasher.verify(stored, candidate)
return True
except argon2.exceptions.VerifyMismatchError:
return False
# Tests
def test_password_round_trip():
h = hash_password('hunter2')
assert verify_password(h, 'hunter2')
assert not verify_password(h, 'wrong')
# ===== 10-25 min: JWT signing + verification =====
# tokens.py
import jwt, time
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization
# In production these come from a vault; here we just generate at import time.
_priv = rsa.generate_private_key(public_exponent=65537, key_size=2048)
_pub = _priv.public_key()
PRIVATE_KEY = _priv.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.NoEncryption(),
)
PUBLIC_KEY = _pub.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo,
)
ISSUER = 'https://api.example.com'
AUDIENCE = 'shop-api'
def issue_access_token(sub: str) -> str:
now = int(time.time())
return jwt.encode(
{ 'sub': sub, 'iss': ISSUER, 'aud': AUDIENCE, 'iat': now, 'exp': now + 900 },
PRIVATE_KEY, algorithm='RS256',
)
def verify_access_token(token: str) -> dict:
return jwt.decode(
token, PUBLIC_KEY,
algorithms=['RS256'], # HARDCODED
audience=AUDIENCE,
issuer=ISSUER,
)
# Tests
def test_token_round_trip():
tok = issue_access_token('u1')
claims = verify_access_token(tok)
assert claims['sub'] == 'u1'
def test_token_rejects_alg_none():
tok = jwt.encode({ 'sub': 'u1', 'aud': AUDIENCE, 'iss': ISSUER }, '', algorithm='none')
try:
verify_access_token(tok)
assert False, 'should have raised'
except jwt.InvalidAlgorithmError:
pass
# ===== 25-40 min: AES-GCM cookie encryption =====
# cookies.py
import os, base64
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
COOKIE_KEY = AESGCM.generate_key(256) # production: from env / KMS
def encrypt_cookie(plain: str, aad: bytes = b'cookie-v1') -> str:
nonce = os.urandom(12)
ct = AESGCM(COOKIE_KEY).encrypt(nonce, plain.encode(), aad)
return base64.urlsafe_b64encode(nonce + ct).decode()
def decrypt_cookie(token: str, aad: bytes = b'cookie-v1') -> str:
raw = base64.urlsafe_b64decode(token.encode())
nonce, ct = raw[:12], raw[12:]
return AESGCM(COOKIE_KEY).decrypt(nonce, ct, aad).decode()
def test_cookie_round_trip():
enc = encrypt_cookie('session=abc')
assert decrypt_cookie(enc) == 'session=abc'
# ===== 40-50 min: HMAC webhook signature =====
# webhook.py
import hmac, hashlib, time
WEBHOOK_SECRET = b'shared-with-the-receiver'
def sign_webhook(body: bytes, ts: int) -> str:
return hmac.new(WEBHOOK_SECRET, f'{ts}.'.encode() + body, hashlib.sha256).hexdigest()
def verify_webhook(body: bytes, sig: str, ts: int) -> bool:
if abs(int(time.time()) - ts) > 300: # 5 min window
return False
expected = sign_webhook(body, ts)
return hmac.compare_digest(expected, sig)
def test_webhook_signature():
body = b'{"order":"o1"}'
ts = int(time.time())
sig = sign_webhook(body, ts)
assert verify_webhook(body, sig, ts)
# tampered body
assert not verify_webhook(b'{"order":"o2"}', sig, ts)
# ===== 50-60 min: wire it up =====
# main.py — FastAPI app exposing the recipes
from fastapi import FastAPI, Header, HTTPException
from pydantic import BaseModel
app = FastAPI()
class LoginReq(BaseModel):
username: str
password: str
# In production look the hash up from a database.
_user_hash = hash_password('hunter2')
@app.post('/login')
def login(req: LoginReq):
if req.username != 'alice' or not verify_password(_user_hash, req.password):
raise HTTPException(401)
return { 'access_token': issue_access_token('u1') }
@app.get('/me')
def me(authorization: str = Header(default='')):
if not authorization.startswith('Bearer '):
raise HTTPException(401)
try:
claims = verify_access_token(authorization[7:])
except jwt.PyJWTError:
raise HTTPException(401)
return { 'sub': claims['sub'] }
# uvicorn main:app
# ===== Post-bootcamp checklist =====
# - password hashing: argon2id, not sha
# - JWT: algorithm pinned at decode
# - encryption: AEAD with random nonce per message
# - HMAC: constant-time compare; timestamp window
# - tests for the misuse path (alg=none, tampered body, wrong password)
# - secrets loaded from env / vault, not committed
# - kms / hsm for production keys
# ===== Pitfalls =====
# - hmac compare with == instead of hmac.compare_digest
# - issue_access_token without 'iat' / 'exp' / 'aud'
# - reusing the same nonce for AES-GCM (catastrophic)
# - rolling your own protocols when libsodium would do
Why it matters
A small repo that demonstrates argon2id + RS256 JWT + AES-GCM + HMAC + constant-time compare in 200 lines of code IS the cryptographic best-practice posture. Most production breaches stem from missing exactly one of these; ship them all once and the rest of your codebase has a clean reference to copy from.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Discussion
Loading…