Applied Cryptography Operations
Definition
Section titled “Definition”Applied cryptography operations is the engineering discipline that keeps cryptography working in a living system: not the mathematics — the inventory, lifecycle, and deprecation machinery that determines whether the crypto actually protects anything at 3 a.m. The distinction matters because systems do not fail at cryptography the way textbooks fail; they fail operationally — the certificate nobody owned expires, the “temporary” MD5 path from 2016 is still hashing password resets, the hardcoded API key ships in a public repo, the TLS 1.0 endpoint lives because a partner’s printer requires it. The discipline rests on four practices: crypto inventory (the bill of materials of algorithms, modes, key lengths, and certificates across code, config, and endpoints — you cannot deprecate what you cannot find), key lifecycle management (generation, custody in HSM/KMS, rotation, revocation, destruction — with the key ceremony and the break-glass procedures documented before they’re needed), certificate operations (issuance, deployment, expiry monitoring, renewal automation — certificates are the most operationally fragile crypto artifact in every estate), and crypto-agility (the architectural capacity to swap algorithms when the floor rises — which is what made the post-quantum migration, NIST’s ML-KEM standardization in 2024, an operations problem rather than a mathematics one). The craft’s center: cryptography fails in deployment, not in theory — the adversary who cannot break AES-256 simply steals the key, and the operations discipline is what stands between the key and the theft.
Why It Matters
Section titled “Why It Matters”The threat model against production crypto is almost entirely operational. The mathematical attacks that make headlines (the SHAttered SHA-1 collision in 2017, the MD5 collision family before it) matter because they convert “theoretically deprecated” into “practically attackable” — but the incidents come from the operational layer: Heartbleed (CVE-2014-0160) leaked keys through a memory-safety bug, not through AES; hardcoded credentials leak through git history; padding-oracle classes (POODLE and relatives) exploit implementation and configuration, not the underlying primitives; and the 64-bit block-cipher birthday problem (Sweet32) exploits ciphers that should have been retired years earlier. The regulatory floor keeps rising — TLS 1.0 and 1.1 are formally dead (RFC 8996), RC4 is prohibited in TLS (RFC 7465), SHA-1 signatures are rejected by browsers and HSM vendors — and every floor that rises converts an installed base into a vulnerability-management backlog that BT11’s cycle has to absorb.
The operational failure modes are consistent across every estate. The certificate incident: the wildcard cert that expired on a Saturday because it was registered to a departed employee’s email — the outage nobody detected until customers did; expiry monitoring is trivial engineering that estates still skip. The inventory gap: the crypto you know about (the TLS terminators, the database encryption) versus the crypto you don’t (the vendor agent hashing tokens with MD5, the batch job with the embedded key, the internal service still on 3DES because it predates the policy) — the un-inventoried crypto is the breach evidence chain’s weak link. Key custody failures: keys in source code, keys in config files, keys on the same disk as the data they encrypt — every one converts “encrypted at rest” into a checkbox rather than a control. Randomness failures: random.random() seeding session tokens (the INSECURE-RAND class) — the entropy failure that makes everything downstream predictable. The deprecation deadlock: “we can’t remove TLS 1.0, a partner depends on it” — five years running; the deadlock breaks with engineering (dual-termination windows, partner engagement) or it breaks with an incident.
Detailed Explanation
Section titled “Detailed Explanation” CRYPTO OPERATIONS STACK +------------------------------------------------------+ | INVENTORY code scan (cryptoscan) + endpoint scan | | (certcheck) + config/protocol census -> crypto BOM | +------------------------------------------------------+ | ALGORITHM FLOOR forbidden list (MD5/SHA-1/RC4/ | | 3DES/TLS1.0-1.1) + approved list + exceptions w/ | | expiry dates (exceptions without expiry = forever) | +------------------------------------------------------+ | KEY LIFECYCLE generate (HSM/KMS) -> custody -> | | rotate (scheduled) -> revoke -> destroy; break-glass| | documented; no keys in source, config, or images | +------------------------------------------------------+ | CERT OPERATIONS inventory -> expiry telemetry | | -> automated renewal (ACME) -> deploy -> verify | +------------------------------------------------------+ | AGILITY abstraction layers, cipher-suite config | | externalized, migration runbooks (the PQC lesson) | +------------------------------------------------------+The inventory is the program’s spine. Three scans, one bill of materials: the code scan (cryptoscan — pattern sweep for weak hashes, weak ciphers, ECB mode, short RSA keys, insecure randomness, hardcoded credentials), the endpoint scan (certcheck — what protocols and certificates the estate actually presents), and the config census (cipher-suite strings, key-size settings, KMS references across infrastructure-as-code). The BOM is risk-ranked by exposure: internet-facing TLS terminators first, internal service meshes second, offline archives last. The scan’s output needs triage before it is truth — an MD5 in a dedupe checksum is not the MD5 in a password reset, and the tool’s findings route to system owners who confirm context; the untriaged sweep becomes noise within a quarter, and the noise is how the next deprecated algorithm survives.
The algorithm floor is policy with engineering teeth. A forbidden list (MD5 and SHA-1 for security purposes, RC4, 3DES, TLS 1.0/1.1, RSA below 2048 bits, ECB mode for anything structured), an approved list (AES-256-GCM, ChaCha20-Poly1305, SHA-256 and up, TLS 1.2+ with modern suites, RSA-3072/ECDSA-P256 and up), and — the part that makes it operations — the exception register: every deviation carries an owner, a compensating control, and an expiry date, because an exception without an expiry is a permanent policy hole wearing a temporary badge. The floor rises on a schedule you control or on an adversary’s schedule you don’t; the deprecation program (dual-termination periods, partner migration windows, telemetry on who still uses the old path) is what converts a floor-raising from an incident into a release cycle.
Key lifecycle is where the real protection lives. Generation inside HSM or KMS (keys never exist outside the custody boundary in extractable form where policy forbids it), rotation on schedule with the rotation tested (the rotation that has never run is the outage that waits), revocation paths drilled, destruction documented. The custody rules are absolute: no keys in source control (cryptoscan’s HARDCODED-KEY class exists because the pattern keeps recurring), no keys in container images or AMIs, no key and data on the same compromised-able boundary, per-environment key separation so staging never shares production custody. The key-compromise playbook (revoke, rotate, re-encrypt what re-encryption can reach, assess what the exposure window means — S10’s breach-scoping for key material) is written before the compromise, because the middle of one is the wrong time to discover which systems can re-encrypt.
Deep dive: the post-quantum migration is an operations rehearsal
NIST standardized the post-quantum candidates — ML-KEM (FIPS 203) and ML-DSA (FIPS 204) among them — in 2024 [verify current status], and the migration is the best rehearsal crypto operations ever gets, because the failure modes are purely operational: libraries that lag, protocols that need negotiation (hybrid classical+PQ key exchange), hardware that can’t be upgraded, performance budgets that change (larger keys, larger signatures), and a decade-long tail of embedded devices that will outlive the migration. The operations lessons transfer to every future floor-raising: inventory first (you cannot migrate keys whose existence is a surprise), agility as architecture (the services that externalized their cipher configuration migrate in a release; the ones with crypto woven into business logic fork their own), harvest-now-decrypt-later as the deadline that already passed (adversaries recording today’s TLS traffic for tomorrow’s quantum decryption makes long-lived secrets the urgent migration class, not the public-key ceremony), and exception registers with expiry dates (the PQ exception that says “until the vendor ships” needs the date, or it becomes the next SHA-1). The program that runs this migration well is the program whose crypto operations were already honest.
Common Techniques
Section titled “Common Techniques”| Technique | What it does | Where it fails | Notes |
|---|---|---|---|
| Crypto BOM (3 scans) | Code+endpoint+config census | Untriaged findings | Spine of the program |
| Algorithm floor | Forbidden/approved lists | Exceptions without expiry | Policy with teeth |
| Exception register | Time-boxed deviations | Permanent exceptions | Owner + control + date |
| HSM/KMS custody | Keys never extractable | Keys beside their data | Per-env separation |
| Scheduled rotation | Keys/certs rotate on clock | Untested rotation | Drill the rotate path |
| Expiry telemetry | Days-to-expiry alerts | Email-registered certs | The Saturday outage |
| ACME automation | Certificates renew themselves | Manual renewals | Renewal ≠ deployment |
| Crypto-agility layers | Cipher config externalized | Crypto in business logic | PQC rehearsal |
| Compromise playbook | Revoke/rotate/re-encrypt | Written mid-incident | S10 scoping joins |
| Deprecation program | Dual-run windows, partner plans | The “partner depends on it” deadlock | Five years is not temporary |
Full Working Code
Section titled “Full Working Code”cryptoscan — the static leg of the crypto inventory: walks a source or configuration tree and flags the six defect classes that make up most operational crypto debt (weak hashes, weak ciphers, ECB mode, short RSA keys, insecure randomness, hardcoded credentials). It is a sweep, not a verdict — findings route to owners for triage, because context (dedupe checksum vs password hashing) decides severity.
#!/usr/bin/env python3"""cryptoscan — weak-crypto static scanner for source/config trees.
Walks a directory (text files under a size cap) and flags: WEAK-HASH md5/sha1 usage WEAK-CIPHER des, 3des, rc4, blowfish ECB-MODE ECB mode on block ciphers WEAK-RSA RSA key sizes below 2048 bits INSECURE-RAND random.random/randint/getrandbits, Math.random HARDCODED-KEY key/secret/password assignments with literalsExit 0 clean, 1 findings, 2 input error. Stdlib only."""import argparseimport reimport sysfrom collections import Counterfrom pathlib import Path
PATTERNS = [ ("WEAK-HASH", re.compile(r"\b(md5|sha1|sha-1)\b", re.I)), ("WEAK-CIPHER", re.compile( r"\b(des|3des|triple-?des|rc4|blowfish)\b", re.I)), ("ECB-MODE", re.compile(r"\bECB\b|['\"]ECB['\"]|/ecb\b")), ("WEAK-RSA", re.compile( r"\brsa\b.{0,40}?\b(1024|1536|768)\b", re.I)), ("INSECURE-RAND", re.compile( r"random\.random\(\)|random\.randint|random\.getrandbits|" r"Math\.random\(\)")), ("HARDCODED-KEY", re.compile( r"(secret|password|api_?key|private_?key|token)\s*[=:]\s*" r"['\"][^'\"]{8,}['\"]", re.I)),]SKIP_DIRS = {".git", "node_modules", ".venv", "__pycache__", "vendor"}MAX_BYTES = 512 * 1024
def scan(path): findings = [] try: text = path.read_text(encoding="utf-8", errors="replace") except OSError as exc: return [(str(path), "READ-FAIL", str(exc))] for lineno, line in enumerate(text.splitlines(), 1): for kind, pat in PATTERNS: if pat.search(line): findings.append( (str(path), kind, f"L{lineno}: {line.strip()[:60]}")) return findings
def main(): ap = argparse.ArgumentParser(description="weak crypto scanner") ap.add_argument("root", type=Path) ap.add_argument("--out", type=Path, default=Path("cryptoscan_out")) args = ap.parse_args() if not args.root.is_dir(): sys.exit(f"fail-loud: not a directory: {args.root}")
findings = [] for path in sorted(args.root.rglob("*")): if not path.is_file() or path.stat().st_size > MAX_BYTES: continue if any(part in SKIP_DIRS for part in path.parts): continue findings.extend(scan(path))
args.out.mkdir(parents=True, exist_ok=True) with (args.out / "findings.csv").open("w", encoding="utf-8") as fh: fh.write("file,kind,detail\n") for f, k, d in findings: fh.write(f'"{f}",{k},"{d}"\n') kinds = Counter(k for _, k, _ in findings) nfiles = len({f for f, _, _ in findings}) print(f"findings={len(findings)} files={nfiles} " f"{dict(kinds) if kinds else ''}") for f, k, d in findings[:12]: print(f" [{k:<14}] {Path(f).name:<20} {d[:56]}") print(f"register: {args.out / 'findings.csv'}") print("sweep output needs triage: a dedupe checksum is not a " "signature — route each finding to its owner") sys.exit(1 if findings else 0)
if __name__ == "__main__": main()Code Walkthrough
Section titled “Code Walkthrough”The pattern table (lines 93-109) encodes the six classes as compiled regexes — deliberately broad, because the sweep’s contract is recall over precision: the HARDCODED-KEY pattern will match test fixtures and the WEAK-HASH pattern will match a comment about MD5 history, and both of those belong in front of a human once, triaged, and then suppressed in the tracker — not silently absent from a scan. The scanner core (lines 111-121) reads with errors="replace" (a binary file that happens to match a pattern is a finding; a crash mid-tree is not), caps file size (the minified bundle that stalls the sweep), and records line numbers so the finding routes to an exact place. The directory walk (lines 133-139) skips the directories that contain other people’s crypto debt (vendor, node_modules) — the scan is for your debt; vendored code gets its own upstream review. Exit 1 (line 155) makes the tool CI-native in the strict sense: new findings fail the build, which forces the triage decision (fix, suppress-with-ticket) at introduction time — the only moment it is cheap.
How It Reaches The Target
Section titled “How It Reaches The Target”The program’s rhythm: cryptoscan runs in CI (new findings block) and monthly across the full estate (trend: findings down, suppressed-with-reason up); certcheck runs daily against the endpoint inventory (expiry thresholds at 21 and 7 days feed different alert tiers), and the config census runs on infrastructure-as-code changes. The crypto BOM review is quarterly — new floor proposals, exception-register expiries coming due, rotation coverage.
The incident that exercises the machinery: a bug-bounty report arrives (S12’s pipeline) — a hardcoded KMS credential in a public JavaScript bundle, HARDCODED-KEY class, present since a 2023 refactor. The compromise playbook runs: revoke the credential at the KMS (minutes, because custody is centralized), rotate the dependent keys, and scope the exposure — the key’s CloudTrail usage log answers what it touched in the exposed window, and the answer (read-only on two non-prod buckets, no personal-data stores — S10’s map confirms) turns a scary report into a contained, documented incident. The post-incident loop: the bundle’s build pipeline gets a secrets gate (the same HARDCODED-KEY pattern, wired pre-publish), the finding class gets a BT9 detection note (KMS calls from anomalous principals), and the exception that allowed “build-time convenience credentials” expires — the register doing exactly what expiry dates are for.
Exploitation Chain
Section titled “Exploitation Chain”- Inventory — three scans (code, endpoints, config) → risk-ranked crypto BOM
- Floor — forbidden/approved lists; exception register with owners and expiry dates
- Deprecate — scheduled removal with dual-run windows and partner migration plans
- Custody — HSM/KMS generation, no source keys, per-environment separation, rotation tested
- Certificates — expiry telemetry, ACME automation, deploy-and-verify loop
- Verify — CI scans block new debt; daily endpoint posture; drift alarms
- Respond — key-compromise playbook: revoke, rotate, re-encrypt, scope (S10 join)
- Measure — BT20: weak-algo trend, cert-expiry incidents (target zero), rotation coverage
Building Your Own Tool
Section titled “Building Your Own Tool”The endpoint leg of the inventory is certcheck: reads a targets file (host:port per line), connects with the stdlib TLS stack, and reports per endpoint — days to certificate expiry, issuer, whether the server still accepts the dead protocols (TLS 1.0/1.1), and connection/trust failures. The “still accepts” probe matters: a modern client negotiates TLS 1.3 and never sees the server’s legacy support, so the check that only reports what it negotiated lies by omission.
Design decisions
Section titled “Design decisions”Two connections per target, on purpose: the modern connection (default context, verification on) answers what a real client gets — expiry, issuer, and any trust failure; the legacy probe (client context forced down to TLS 1.0/1.1, verification off, security level relaxed where the platform allows) answers what the server would still accept from an attacker or a partner’s ancient stack. Expiry uses the parsed notAfter from the verified peer certificate and reports days-to-expiry — the number that drives the alert tiers. Every failure is a finding, never a crash: an unreachable host is CONN, a verification failure is TRUST (its own class — expired, self-signed, and hostname-mismatch all land there and the operator reads the message), so one dead endpoint cannot take down the sweep. The exit code encodes the operational rule: any finding means exit 1, which lets a daily cron page on threshold breaches while the register (posture.csv) feeds the trend.
Full source
Section titled “Full source”#!/usr/bin/env python3"""certcheck — TLS endpoint posture checker.
Input targets.txt: host[:port] per line (# comments allowed).Per endpoint: modern connection: expiry days, issuer, negotiated protocol legacy probe: does the server still accept TLS 1.0/1.1?Findings: EXPIRY (below threshold), PROTOCOL (dead version accepted), TRUST (verification failure), CONN (unreachable).Output: posture report + certcheck_out/posture.csv. Stdlib only."""import argparseimport csvimport socketimport sslimport sysfrom datetime import datetime, timezonefrom pathlib import Path
DEAD = {"TLSv1", "TLSv1.1"}
def legacy_probe(host, port): """Return the protocol version an ancient client would negotiate.""" try: ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE ctx.minimum_version = ssl.TLSVersion.TLSv1 ctx.maximum_version = ssl.TLSVersion.TLSv1_1 try: ctx.set_ciphers("DEFAULT@SECLEVEL=0") except ssl.SSLError: return None # platform forbids legacy entirely with socket.create_connection((host, port), timeout=5) as sock: with ctx.wrap_socket(sock, server_hostname=host) as tls: return tls.version() except (ssl.SSLError, ssl.SSLEOFError, OSError): return None
def check(host, port, days_threshold): findings, days, issuer = [], None, "?" ctx = ssl.create_default_context() try: with socket.create_connection((host, port), timeout=5) as sock: with ctx.wrap_socket(sock, server_hostname=host) as tls: proto = tls.version() or "?" if proto in DEAD: findings.append(("PROTOCOL", f"negotiated {proto} to modern client")) cert = tls.getpeercert() issuer = (dict(x[0] for x in cert.get("issuer", ())) .get("organizationName", "?")) not_after = cert.get("notAfter") if not_after: exp = datetime.strptime( not_after, "%b %d %H:%M:%S %Y %Z") exp = exp.replace(tzinfo=timezone.utc) days = (exp - datetime.now(timezone.utc)).days if days < days_threshold: findings.append(("EXPIRY", f"{days}d to expiry")) except ssl.SSLCertVerificationError as exc: findings.append(("TRUST", str(exc.verify_message or exc))) return findings, None, "?" except (socket.timeout, OSError) as exc: findings.append(("CONN", f"unreachable: {exc}")) return findings, None, "?" return findings, days, issuer
def main(): ap = argparse.ArgumentParser(description="TLS posture checker") ap.add_argument("targets", type=Path) ap.add_argument("--days", type=int, default=21, help="expiry warning threshold (default 21)") ap.add_argument("--out", type=Path, default=Path("certcheck_out")) args = ap.parse_args() if not args.targets.exists(): sys.exit(f"fail-loud: targets not found: {args.targets}") targets = [] for line in args.targets.read_text(encoding="utf-8").splitlines(): line = line.strip() if line and not line.startswith("#"): host, _, port = line.partition(":") targets.append((host, int(port or 443))) if not targets: sys.exit("fail-loud: no targets parsed")
rows = [] for host, port in targets: findings, days, issuer = check(host, port, args.days) legacy = legacy_probe(host, port) if legacy in DEAD: findings.append(("PROTOCOL", f"still accepts {legacy} (RFC 8996)")) detail = ("; ".join(f"{k}:{d}" for k, d in findings) if findings else "OK") rows.append({"host": f"{host}:{port}", "issuer": issuer, "days_to_expiry": days, "findings": detail}) print(f" {host}:{port} issuer={issuer} " f"expiry={days if days is not None else '?'}d {detail}")
args.out.mkdir(parents=True, exist_ok=True) with (args.out / "posture.csv").open("w", newline="", encoding="utf-8") as fh: w = csv.DictWriter( fh, fieldnames=["host", "issuer", "days_to_expiry", "findings"]) w.writeheader() w.writerows(rows) bad = sum(1 for r in rows if r["findings"] != "OK") print(f"targets={len(rows)} clean={len(rows) - bad} findings={bad}") print(f"register: {args.out / 'posture.csv'}") print("doctrine: expiry is a scheduled event, not an outage — " "alert at 21 days, escalate at 7") sys.exit(1 if bad else 0)
if __name__ == "__main__": main()Walkthrough
Section titled “Walkthrough”The legacy probe (lines 217-233) is the tool’s reason to exist: a client context pinned to TLSv1..TLSv1_1 with verification off and SECLEVEL=0 (the platform relaxation that makes the handshake possible at all — and if the platform refuses, the probe honestly returns None, meaning “cannot test” rather than “clean”). The modern check (lines 236-263) does the opposite: full verification, so a trust failure is itself a TRUST finding rather than an exception — SSLCertVerificationError carries the reason (expired, self-signed, mismatch) in verify_message, which is the operator’s answer. Expiry math (lines 249-256) parses notAfter (the "Jun 3 ..." double-space format parses fine under %b %d), converts to UTC, and compares against the threshold — the number that becomes the alert tier. The target parser (lines 275-282) accepts host and host:port with comments, defaulting to 443, and an unparsable port fails loud rather than silently scanning nothing. The summary (lines 305-309) splits clean from findings and writes the register for the daily trend.
Compilation and test procedure
Section titled “Compilation and test procedure”serve a local TLS endpoint with a short-lived self-signed cert for the drill
Section titled “serve a local TLS endpoint with a short-lived self-signed cert for the drill”$ openssl req -x509 -newkey rsa:2048 -nodes -days 10 -subj “/CN=localhost”
-keyout key.pem -out cert.pem 2>/dev/null
$ python3 -c ”
import ssl,socket,threading
ctx=ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER);ctx.load_cert_chain(‘cert.pem’,‘key.pem’)
s=socket.socket();s.bind((‘127.0.0.1’,8443));s.listen()
def serve():
while True:
c,_=s.accept()
try: ctx.wrap_socket(c,server_side=True).recv(64)
except Exception: pass
threading.Thread(target=serve,daemon=True).start();import time;time.sleep(600)” &
$ echo -e “127.0.0.1:8443\ngithub.com:443\n# comments ignored” > targets.txt $ python3 certcheck.py targets.txt —days 21 127.0.0.1:8443 issuer=? expiry=?d TRUST:self-signed certificate github.com:443 issuer=Sectigo Limited expiry=58d OK targets=2 clean=1 findings=1 register: certcheck_out/posture.csv
note the TRUST endpoint reports expiry=?d — an unverified chain yields no
Section titled “note the TRUST endpoint reports expiry=?d — an unverified chain yields no”trusted notAfter; read it directly with: openssl x509 -in cert.pem -noout -enddate
Section titled “trusted notAfter; read it directly with: openssl x509 -in cert.pem -noout -enddate”Detection considerations
Section titled “Detection considerations”The two scanners join at the BOM: cryptoscan covers what the estate contains (code, config), certcheck covers what it presents (endpoints) — and the trends matter more than the snapshots: weak-algo findings trending down with suppressions documented is the deprecation program working; findings flat is the program stalled. The security-signal layer rides on top: certificate-expiry telemetry is the outage-prevention control (alert at 21 days, escalate at 7, page at 2), and the runtime signals for key compromise join the identity plane — Windows Security 4692/4694 (DPAPI master-key backup/protection events) on Windows estates, KMS/HSM API anomaly detection (calls from anomalous principals — the S9 service-account baseline pattern), and Kerberos 4768s with PKINIT for certificate-auth anomalies [verify per environment]. TLS-level network signals (Suricata custom/local sid:1000001+ for legacy-protocol handshakes on ports that should be modern) catch the clients that never got the migration memo. BT11’s cycle owns the floor-raising backlog; BT15 owns the certificate-auth identity joins.
Improvements to add
Section titled “Improvements to add”- Add cipher-suite enumeration per endpoint (the full suite list, not just protocol) and diff against the approved-suite list.
- Add certificate-chain depth and cross-signature reporting (the intermediate-expiry outage class).
- Add SAN coverage check: does the certificate cover every hostname the service actually answers on?
- Join with Certificate Transparency logs: detect certificates issued for your domains that you didn’t issue.
- Add a trends mode: run against last cycle’s posture.csv and report drift (new findings, resolved findings, expiry cliff-edges).
Detection — Defender View
Section titled “Detection — Defender View”| Signal | Source | ID | Notes |
|---|---|---|---|
| WEAK-HASH / HARDCODED-KEY | cryptoscan | per CI cycle | Triage before severity |
| Expiry below threshold | certcheck | daily | 21d/7d/2d tiers |
| Dead protocol accepted | certcheck | daily | RFC 8996 class |
| TRUST failures | certcheck | per connect | Self-signed/expired/mismatch |
| DPAPI key backup events | Windows Security | 4692/4694 | Key-export class |
| KMS/HSM anomalous calls | Cloud audit + S9 baseline | realtime | Off-playbook usage |
| Legacy TLS handshakes | Suricata custom/local | sid:1000001+ | Migration stragglers |
| CT log surprises | CT monitor | per cert | Certificates you didn’t issue |
| Rotation overdue | Key registry | weekly | Custody coverage |
| Exception register expiries | BOM review | quarterly | The forever-exception check |
Evasion Techniques
Section titled “Evasion Techniques”Adversaries do not break modern primitives; they evade the operations around them. Steal, don’t crack: the key in memory (Heartbleed-class), the key in the repo, the key in the CI variable — the attack surface is custody, and the counters are the custody rules (HSM/KMS extraction policy, secret scanning, per-env separation) plus the assumption that any key a process holds is a key an adversary holding that process holds. Downgrade: forcing legacy protocols or weak suites where the server still accepts them (certcheck’s probe is the standing test) — the counter is disabling, not preferring, and the TLS config that “prefers” modern while allowing legacy is the evasion’s home. Oracle classes: padding oracles (the POODLE/Lucky13 lineage) and Bleichenbacher-style RSA decryption oracles live in implementations and error messages — constant-time verification, generic error handling, and keeping the TLS stack patched are the controls; the library that leaks “which padding failed” in a timing difference is the vulnerability. Nonce and IV reuse: the GCM nonce repeated under the same key (the catastrophic failure mode of counter management done by hand) — nonce generation belongs to vetted library internals, never to application code. Algorithm confusion: JWT alg=none and key-type confusion classes — libraries that let the attacker pick the algorithm fail; pin algorithms at verification time. Validity-window games: the adversary with a CA-position (or a mis-issued certificate — the CT-monitor finding) inherits trust that no endpoint scan can distinguish from legitimate — which is why certificate transparency monitoring exists, and why the CA compromise playbook (pin, revoke, re-issue at velocity) is rehearsed like the key-compromise one.
⚠️ What caught me: our expiring asset wasn’t a certificate — it was the intermediate. The wildcard renewed on schedule (automation working, alerts green, my dashboards spotless), and then on a Sunday the leaf certificates started failing chain validation everywhere: the intermediate CA that issued them had expired, and no monitoring anywhere watched intermediates because “we don’t own those.” We rebuilt chains on four services at 2 a.m. from a vendor’s support page. The inventory lesson is now permanent: certcheck reports the whole presented chain, and the BOM tracks every link we depend on, owned or not — because the adversary doesn’t care whose name is on the artifact, and neither does a chain validator.
Lab Practice
Section titled “Lab Practice”Build the crypto-operations loop on your own (or sandbox) estate. Scan: run cryptoscan against a small tree seeded with one of each defect class; triage every finding and write the one-line disposition (fix/suppress-with-ticket) for each. Posture: stand up the local TLS drill endpoint from the terminal section; run certcheck against it and one real public site; confirm the TRUST finding on the self-signed endpoint and the expiry math within a day. Floor: draft your forbidden/approved lists and the exception-register template (owner, compensating control, expiry date); put one real exception in it with a real date. Compromise drill: tabletop the hardcoded-key incident from this module — revoke, rotate, scope — and time the revoke step. Debrief: which crypto in your environment would the three scans miss today, and who owns the intermediate certificates you depend on?
Interview Questions
Section titled “Interview Questions”- Technical: Why is
random.random()fatal for token generation, and what distinguishes a CSPRNG-backed implementation from a statistically-random one? - Technical: Explain harvest-now-decrypt-later and why it makes long-lived secrets the urgent post-quantum migration class even before quantum computers exist.
- Scenario: A HARDCODED-KEY finding shows a live KMS credential in a public bundle. Walk the first hour: revoke, rotate, scope — and what the exception register did wrong to allow it.
- Scenario: Sunday, chain-validation failures on four services. The leaf certificates are valid. What expired, why did monitoring miss it, and what does certcheck need to report?
- Conceptual: Why is crypto-agility an architecture property rather than a library choice — and what did the post-quantum migration reveal about the estates that lacked it?
- Deep-dive: Design the crypto-operations program for a 200-service estate: the three scans, the BOM, the floor and exception register, key custody, certificate operations, and the deprecation machinery.
- Practical: You inherit an estate with no crypto inventory and a “partner depends on TLS 1.0” deadlock that is five years old. What do you scan first, and how do you break the deadlock without breaking the partner?
Related Modules
Section titled “Related Modules”BT11 (Vulnerability Management) · BT15 (Identity & MFA Defense) · S10 (Data Privacy & Protection Engineering) · H11 (Hardware Security Controllers)