Audit cryptography implementation — algorithm choice, key sizes, KDF parameters, IV/nonce handling, signature verification, randomness, TLS configuration, and key rotation. Deeper than owasp-audit A04. Use when the user mentions 'crypto review,' 'cryptography audit,' 'encryption review,' 'KDF,' 'PBKDF2,' 'Argon2,' 'bcrypt cost,' 'IV reuse,' 'nonce reuse,' 'AES mode,' 'AES-GCM,' 'AES-ECB,' 'signature verification,' 'TLS configuration,' 'cipher suites,' 'key rotation,' 'libsodium,' 'BoringSSL,' or 'is this crypto right.'
94%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Low
Low-risk findings worth noting
Audit how cryptography is implemented in an application — algorithm choices, parameters, modes, and the implementation patterns that turn good primitives into broken systems. Deeper than owasp-audit A04 Cryptographic Failures (which catches the obvious "MD5 password" and "VERIFY_NONE" cases). This skill is for the subtler implementation review.
Most crypto failures are not "they used MD5." Most failures are: right primitive, wrong mode (ECB instead of GCM), right algorithm, wrong parameter (PBKDF2 with 1,000 iterations in 2026), right library, wrong call order (init the cipher after the data was loaded).
Cross-references: owasp-audit A04 (baseline) + A07 (timing-safe comparison), secrets-audit (key storage), iam-audit (KMS / HSM patterns), native-code-review (memory safety in C, C++ and Rust crypto code, and wiping secrets from memory), fuzzing-harness (round-trip and tamper-rejection property tests for encode/decode and sign/verify).
Review code, configuration and key-management setup the user owns or is authorized to audit. TLS probing (testssl.sh, openssl s_client) is a live check: run it only against endpoints the user is authorized to test; it isn't pre-approved, so it prompts every time. Never try to recover keys, crack hashes or decrypt data you weren't given access to.
The default audit verdict for any custom encryption scheme is "use libsodium / Tink / WebCrypto instead." There are < 50 people on Earth who can design new crypto safely, and they don't work at your company. Unless an explicit threat model says otherwise, custom crypto is a finding.
hash(key + message)MD5, SHA1 (outside of HMAC-SHA1 in legacy compat), DES, RC4, Blowfish, AES.*ECB, pkcs1_v1_5 (Python), RSA.encrypt without OAEPPBKDF2 (check iteration count), HKDF, Argon2, scrypt, pbkdf2_hmac (Python; check iterations arg)Wrong IV / nonce handling is one of the top three sources of "the crypto looks right but actually leaks plaintext."
iv = "0000000000000000", iv = bytes(16)), zero-IV constructors, counter resetsCipher.getInstance("AES") (Java default is ECB), AES.new(key) (PyCryptodome default is ECB), crypto.createCipher (Node, deprecated, derives IV from key — DON'T)crypto.timingSafeEqual in Node, hmac.compare_digest in Python, subtle.ConstantTimeCompare in Go) — see owasp-audit A07alg is exactly what your code expects. Never call jwt.decode and use the claims without jwt.verify. Many libraries accept alg: none (un-signed) by default — verify the library version doesn't have thisowasp-audit A04 type-coercion in signature paths (parseInt → NaN)jwt.decode (without subsequent verify), alg: 'none', verify.*sig without timingSafeEqual nearbycrypto.randomBytes (Node), secrets.token_bytes (Python ≥ 3.6), crypto/rand (Go), SecRandomCopyBytes (iOS), SecureRandom (Java)Math.random() (Node — Mersenne Twister, predictable), random.random() (Python — same), rand() (C — terrible), arc4random_uniform for crypto (BSD — historical name only, but verify the runtime)Math.random, random.random, rand(, mt_rand (PHP), Random.new (Ruby)VERIFY_PEER, full chain, hostname check enabled (see owasp-audit A04 for managed-service caveat)testssl.sh https://target or sslyze --regular targetMessageVerifier / MessageEncryptor use modern primitives by default; verify they're configured with a strong key (32 bytes / 256 bits)cryptography.fernet is AES-128-CBC + HMAC-SHA256 (acceptable but not GCM); django.core.signing for short signed valuesiron-session / cookie-signature over rolling your ownCommonCrypto works but has more footgunsClose every candidate from the checklist with one verdict, using the shared format in references/findings-format.md. Before anything is confirmed, run the refutation pass in references/verification.md: open the call site, find where the key, IV, salt and parameters actually come from, and check whether the library version in use already applies a safe default.
A weak primitive is a finding when you can name who gains what: the lower-trust principal (anyone who sees ciphertext, a holder of a leaked hash table, a client that sends a token, a network attacker on the path), the control it defeats (confidentiality, integrity, authentication, password secrecy) and the result. Rate severity with references/severity-calibration.md by the data the broken control protects. Conditions such as "attacker already holds a copy of the users table" go in conditions[] (data_state); they lower likelihood, not impact.
confirmed — the code shows the broken use and the result follows. Examples:
createCipheriv('aes-256-gcm', key, FIXED_IV) in src/lib/vault.ts with one key for every record: anyone who reads the encrypted column gets the XOR of plaintexts and can forge ciphertexts. A unit test that encrypts two values and shows the repeated keystream sets reproduced: true.Math.random() in src/lib/session.ts: predictable tokens for any user's session.jwt.sign(..., process.env.JWT_SECRET || 'dev-secret'): confirmed with a system_configuration condition ("deployments that don't set JWT_SECRET") and confidence medium. Anyone who reads the repo can mint tokens for those deployments.data_state condition for the hash-table leak.needs_validation — the code supports it, but one fact lives outside it. Examples:
nginx.conf allows TLSv1 TLSv1.1, but a CDN or load balancer may terminate TLS first. Blocker: which config serves api.example.com. Plan: testssl.sh --protocols https://api.example.com (live check, needs authorization). A TLS check described but not run stays here.kms:Decrypt. Plan: aws kms get-key-policy --key-id <id> --policy-name default.rejected — a named control at a named location rules it out. Examples:
jwt.verify(token, key) with no algorithms option, ruled out by the library: control is "jsonwebtoken 9.0.2 verify() rejects alg: none and HS/RS confusion for asymmetric keys", ruled_out_by.location is the package-lock.json line that pins 9.0.2.createHash('md5') in src/lib/etag.ts, used only as a cache ETag. ruled_out_by.location is the only call site, which sets a response header and makes no trust decision.Hardening notes, not records: a weaker-than-recommended choice with no traced exposure. Examples: bcrypt cost 10 instead of 12, Encrypt-then-MAC with AES-CBC done correctly (prefer GCM), TLS 1.2 CBC suites offered next to AEAD suites, no OCSP stapling, no rotation schedule on a KMS key, missing HSTS preload. Custom crypto is a finding when you trace what it fails to protect; otherwise it's a "replace, don't patch" hardening note.
Live and cloud evidence. TLS scans and KMS or HSM settings describe deployed resources, not files. Save the read-only output, redacted, to <audit dir>/evidence/<name>.json or .txt (testssl.sh --jsonfile, sslyze --json_out, aws kms describe-key) and point location and evidence[] at that file and line. Never save key material, private keys, plaintext you decrypted or a secret value.
Fingerprint: crypto-audit/<class>/<file or resource>/<symbol or name>, no line numbers:
crypto-audit/gcm-nonce-reuse/src/lib/vault.ts/encryptFieldcrypto-audit/CWE-338/src/lib/session.ts/newSessionIdcrypto-audit/tls-legacy-protocol/host/api.example.com/443crypto-audit/kms-key-policy/aws/123456789012/us-east-1/alias/app-dataWrite host:port as host/<host>/<port> and ARNs with slashes: the validator rejects :<digits> segments.
ext keys (optional, on any record):
| Key | Value | Example |
|---|---|---|
primitive | Algorithm, mode and parameters as used | "AES-256-GCM, static 96-bit IV", "PBKDF2-HMAC-SHA256, 1000 iterations" |
library | Library and version that performs the operation | "node:crypto (Node 20)", "pycryptodome 3.20.0" |
standard | The guidance the use departs from | "NIST SP 800-38D 8.2", "RFC 8996", "OWASP Password Storage" |
Then write findings.json sorted by fingerprint, validate it, and optionally export SARIF:
node references/validate-findings.mjs findings.json
node references/to-sarif.mjs findings.json -o findings.sariftestssl.sh and curl --tlsv1.3 --tls-max 1.3 https://target (lower-bound TLS version enforcement)Write findings.json first and validate it. Render the report from it, plus your hardening-notes list. The prose never changes a verdict, severity or blocker the JSON doesn't already say.
# Cryptography Implementation Audit
## Project: [name] ## Date: [date]
## Scope: [components covered; full | partial — what was left out, e.g. "no TLS scan run"]
## findings.json: [path] — validated: [yes]
### Summary
- Confirmed: [N] (Critical [n] | High [n] | Medium [n] | Low [n]) Needs validation: [N] Rejected: [N] Hardening notes: [N]
### Confirmed findings
#### [SEVERITY] [Title] `fingerprint`
**Location:** `path:line` **CWE:** [CWE-n] **Confidence:** [medium | high (reproduced)] **Primitive:** [ext.primitive]
**What and why it matters:** [principal, control defeated, result]
**Conditions:** [e.g. data_state: attacker holds the users table]
**Vulnerable code:** [snippet]
**Fix:** [concrete change, including migration of existing ciphertext or hashes]
**Verify the fix:** [round-trip test, rehash on login, testssl output; see Verify Fixes at Runtime]
### Needs validation
| Finding | Blockers | Check for the owner to run |
|---|---|---|
### Hardening notes (not findings)
- [location] — [gap] — [why it isn't a finding] — [effort]
### Rejected (do not re-raise)
| Fingerprint | Claim | Ruled out by | Location |
|---|---|---|---|
### TLS posture (if applicable)
[Summary of saved testssl.sh / sslyze output, with the evidence file path, or "not run"]
### Key inventory
| Key | Purpose | Location | Algorithm | Rotation |
|-----|---------|----------|-----------|----------|
### Prioritized remediation plan
1. [Critical / High confirmed findings]
2. [Medium / Low confirmed findings]
3. [Owner-run checks for needs-validation items]
4. [Hardening notes worth the effort]Dispositions for confirmed findings (set later, usually by finding-triage): Fixed / Deferred / Accepted Risk / Mitigated (platform/config) / Requires major migration. A false positive is a rejected record, not a disposition.
references/ at build time): findings-format.md, severity-calibration.md, verification.md, validate-findings.mjs, to-sarif.mjs, findings.schema.jsonbriiirussell/cybersecurity-skills
c79bb8b
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.