Context: Discord-hosted winter CTF focused on JWT chaining + gRPC transport. Everything happened on my laptop—no production systems touched.
AI assist: ChatGPT walked me through Libsodium quirks, grpcurl flags, and token-claim gotchas whenever I stalled. I stored each prompt/response in the repo for transparency.
Status: Documenting the lab so classmates (and future-me) can reproduce the ladder without assuming I’ve run auth incidents in the wild.

Reality snapshot

  • The lab shipped starter artifacts (PAGE_TOKEN, secretbox.md, proto files). I supplied the glue code, TLS setup, and troubleshooting.
  • Every token exchange was scripted; nothing was “click next.” I automated decrypt → verify → request to avoid mis-ordering steps under time pressure.
  • The “win” was extracting a final streaming token, not supporting real users. Treat this as a study note, not evidence of production mastery.

What I set up

Files & scaffolding

  • ctf-tools/ctf_jwt_walkthrough.py decrypts the Libsodium payload, verifies claims with PyJWT (audience checks disabled for the lab), and logs timestamps for each hand-off.
  • ctf-tools/grpc_client.py wraps the generated Python stubs so I can swap AUTH_TOKEN via flags (--bootstrap, --unary, --stream).
  • notes/terminal-log.md captures every command, header, and response code so reviewers see exactly what I typed.
  • TLS certs from the challenge live in certs/lab/. I pinned them in grpcurl with --cacert and re-used them in the Python client.

Toolchain

ToolWhy I needed it
Libsodium / PyNaClReplayed the XSalsa20-Poly1305 decrypt (secretbox_open) using the provided key + nonce.
PyJWTVerified signatures, printed claims, and disabled verify_aud when the lab intentionally left audiences blank.
grpcurlFast pokes at unary vs streaming methods; helpful for spotting header mistakes.
Python gRPC clientProduced more detailed stack traces than grpcurl when HTTP 464s popped up.
Wireshark + openssl s_clientConfirmed the TLS handshake + ALPN negotiation; crucial when proxies silently closed streams.

The ladder, step by step

  1. Bootstrap decodejwt.io + PyJWT let me inspect the PAGE_TOKEN. Seeing the kid claim point at secretbox.md confirmed the symmetric key route.
  2. Ciphertext decrypt – Base64 decoded the key/nonce/ciphertext, fed them into Libsodium, and got JWT_TOKEN. I scripted retries because mistyping Base64 once meant starting over.
  3. Token trading – Each call to token.v1.TokenService/GetToken issued a more scoped token (CONNECT_UNARY_TOKEN, NA_CL_SECRET_TOKEN, etc.). I exported whichever token was current to AUTH_TOKEN so subsequent commands read it automatically.
  4. Streaming flagStreamToken refused to cooperate until I normalised every header to lowercase (authorization, te) and forced content-type: application/grpc. Once those matched, the Render-style proxy let the stream through and I grabbed the flag.

Troubleshooting log

SymptomRoot causeHow I fixed it
HTTP 464, zero server logsSent JSON or uppercase headers through the proxyForced lowercase metadata + application/grpc every time.
Token verification failedAudience check still enabledPassed options={"verify_aud": False} to jwt.decode.
gRPC metadata missingUsed Authorization instead of authorizationLowercased the header; ALB stripped the uppercase version.
TLS handshake reset midstreamForgot to pin the provided certAdded --cacert certs/lab/rootCA.pem (and equivalent in Python) before retries.
Manual token swaps caused mistakesCopy/paste fatigueWrote scripts/set-token.sh <token_file> to export the current value.

Evidence that I actually did the work

  • Scripts & notes: ctf-tools/, notes/terminal-log.md, and notes/ai-prompts.md live in the repo so reviewers can replay every command.
  • Packet capture: captures/streamtoken-success.pcapng shows the working HTTP/2 exchange (ClientHello → SETTINGS → HEADERS/DATA).
  • Gist snippet: https://gist.github.com/BradleyMatera/ctf-jwt-notes (redacted secrets) demonstrates the decrypt + verify loop.
  • Prompt log: Lists each ChatGPT conversation that influenced the code so I don’t present AI-generated output as my own insight.

What’s still on the todo list

  • Convert the markdown logs into a repeatable workshop (maybe a README.md with copy/paste commands).
  • Add pytest coverage for the helper scripts and publish them as a ctf-tools package when they’re less fragile.
  • Explore gRPC-Web + Envoy because most browser clients I touch won’t support native gRPC.
  • Build a tiny dashboard showing which token you’re currently holding; right now it’s just environment variables and terminal echoes.

How to replay this lab fast

  • Clone the repo, copy the starter artifacts into ctf-tools/data/, and run python ctf_jwt_walkthrough.py --dump.
  • Run scripts/set-token.sh data/bootstrap.jwt to seed the first request, then use grpc_client.py --route GetToken --out tokens/connect.jwt.
  • Keep notes/terminal-log.md open; it’s the source of truth for expected claims and headers at each hop.
  • If anything fails, check the troubleshooting table above before touching code—the errors were 90% header formatting.

Ethics and safety

  • All secrets are lab-provided and scoped to the CTF. I don’t reuse them or share raw artifacts.
  • Every AI-assisted suggestion that touched crypto or headers was cross-checked against docs; nothing ships on trust alone.
  • This is not production auth. I disable verify_aud because the lab required it; I never do that on real systems.

What I would change next time

  • Build a CLI wrapper to avoid juggling Python + shell scripts.
  • Add property-based tests for the decrypt/verify functions so malformed payloads are caught immediately.
  • Record a Loom walkthrough to show the pace of requests; text alone hides the timing pressure.
  • Swap grpcurl for a minimal Go client to compare error visibility and performance.

Interview angles

  • Debugging under pressure: Explain how I isolated each failure (headers, TLS, audience checks) with concrete evidence.
  • Security mindset: Call out where I intentionally disabled validations for the lab and why that’s unacceptable elsewhere.
  • Automation: Show how scripting the ladder reduced copy/paste errors—mirrors real-world “script the toil” thinking.
  • Communication: Mention the notes/terminal-log.md as a stand-in for incident notes so others can replay the steps.

Checklists I keep close

  • Before decrypting: confirm key length, nonce length, and Base64 padding.
  • Before sending tokens: lowercase headers, include te: trailers, and set content-type: application/grpc.
  • Before trusting AI output: find the spec line; if missing, log it in hallucinations.md.
  • After success: export artifacts, tag them by step, and write a retro while details are fresh.

Open questions

  • Best lightweight way to visualize token flow (sequence diagram vs live dashboard).
  • Whether to refactor to Go/Rust for better perf and type safety without losing readability.
  • How to share this as a workshop without leaking challenge details or encouraging bad security habits.
  • If I should add JWT tampering scenarios (alg=none, key confusion) to make the lab more robust.

References & further reading