Skip to content

Applied Cryptography Operations

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.

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.

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.

TechniqueWhat it doesWhere it failsNotes
Crypto BOM (3 scans)Code+endpoint+config censusUntriaged findingsSpine of the program
Algorithm floorForbidden/approved listsExceptions without expiryPolicy with teeth
Exception registerTime-boxed deviationsPermanent exceptionsOwner + control + date
HSM/KMS custodyKeys never extractableKeys beside their dataPer-env separation
Scheduled rotationKeys/certs rotate on clockUntested rotationDrill the rotate path
Expiry telemetryDays-to-expiry alertsEmail-registered certsThe Saturday outage
ACME automationCertificates renew themselvesManual renewalsRenewal ≠ deployment
Crypto-agility layersCipher config externalizedCrypto in business logicPQC rehearsal
Compromise playbookRevoke/rotate/re-encryptWritten mid-incidentS10 scoping joins
Deprecation programDual-run windows, partner plansThe “partner depends on it” deadlockFive years is not temporary

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 literals
Exit 0 clean, 1 findings, 2 input error. Stdlib only."""
import argparse
import re
import sys
from collections import Counter
from 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()

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.

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.

  1. Inventory — three scans (code, endpoints, config) → risk-ranked crypto BOM
  2. Floor — forbidden/approved lists; exception register with owners and expiry dates
  3. Deprecate — scheduled removal with dual-run windows and partner migration plans
  4. Custody — HSM/KMS generation, no source keys, per-environment separation, rotation tested
  5. Certificates — expiry telemetry, ACME automation, deploy-and-verify loop
  6. Verify — CI scans block new debt; daily endpoint posture; drift alarms
  7. Respond — key-compromise playbook: revoke, rotate, re-encrypt, scope (S10 join)
  8. Measure — BT20: weak-algo trend, cert-expiry incidents (target zero), rotation coverage

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.

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.

#!/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 argparse
import csv
import socket
import ssl
import sys
from datetime import datetime, timezone
from 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()

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.

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”

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.

  1. Add cipher-suite enumeration per endpoint (the full suite list, not just protocol) and diff against the approved-suite list.
  2. Add certificate-chain depth and cross-signature reporting (the intermediate-expiry outage class).
  3. Add SAN coverage check: does the certificate cover every hostname the service actually answers on?
  4. Join with Certificate Transparency logs: detect certificates issued for your domains that you didn’t issue.
  5. Add a trends mode: run against last cycle’s posture.csv and report drift (new findings, resolved findings, expiry cliff-edges).
SignalSourceIDNotes
WEAK-HASH / HARDCODED-KEYcryptoscanper CI cycleTriage before severity
Expiry below thresholdcertcheckdaily21d/7d/2d tiers
Dead protocol acceptedcertcheckdailyRFC 8996 class
TRUST failurescertcheckper connectSelf-signed/expired/mismatch
DPAPI key backup eventsWindows Security4692/4694Key-export class
KMS/HSM anomalous callsCloud audit + S9 baselinerealtimeOff-playbook usage
Legacy TLS handshakesSuricata custom/localsid:1000001+Migration stragglers
CT log surprisesCT monitorper certCertificates you didn’t issue
Rotation overdueKey registryweeklyCustody coverage
Exception register expiriesBOM reviewquarterlyThe forever-exception check

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.

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?

  • 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?

BT11 (Vulnerability Management) · BT15 (Identity & MFA Defense) · S10 (Data Privacy & Protection Engineering) · H11 (Hardware Security Controllers)